Changing input of controlled component changes property value to undefined - javascript

I have an array of default values in a useState. Upon an onChange event via a select, the properties in the array are changed to undefined. I've been reading up on controlled components and I think I've missed something silly.
Here is a codesandbox.
const [config, setConfig] = useState([{
carType: "Sedan",
carColor: "Red"
}]);
// onChange
const setCarType = (e) => {
setConfig({ ...config[0], carType: e.target.value });
};
const { carType, carColor } = config[0];
I thought that I could use the spread operator here in order to copy the two properties of config[0] into two separate constants. I originally had the config in an object but was thrown an error "Objects are not valid as a react child".

const setCarType = (e) => {
setConfig([{ ...config[0], carType: e.target.value }]);
};
const setCarColor = (e) => {
setConfig([{ ...config[0], carColor: e.target.value }]);
};
When you were setting config, you were setting a new object as config but it was initialized as an array of objects. So config[0] was undefined cause config was no more an array. Try the above code, it will solve the issue.

Related

function not returning updated data based on updated state value

I am creating a crypto react application and just trying to figure something out in which a function is returning an empty array when I update one of the state values. I am thinking it is a race condition. It works until there is a state change. Please see my code below:
So I have a state object. And it is returning an array of currencies by default based on the selectedCurrencyState.selectedFromCurrency value, which is a state value. This function basically needs to determine the quoteAsset before returning the object that contains the array of baseCurrencies. swapInfo is the state value containing the mock data. The mock data is nested like so:
const getBaseCurrencies = () => {
const filteredQuoteAssetObj = selectedCurrencyState.selectedFromCurrency
&& swapInfo.find(item =>
item.quoteAsset === selectedCurrencyState.selectedFromCurrency);
const currencies = filteredQuoteAssetObj
&& filteredQuoteAssetObj.baseAssets.map(item => item.baseAsset);
return currencies;
}
Here is the mock data that I am using:
Like I said, it works until the state change of selectedCurrencyState.selectedFromCurrency(switch state from USD to SHIB). Afterwards, it returns an empty array. By default I have the state set to USD as the selectedFromCurrency.
Any help or input is appreciated. Thanks guys!
--UPDATED--
Here is the way I am updating the state by the way. I am using a functional component therefore, using useState.
const [swapInfo, setSwapInfo] = useState([]);
const [fromCurrencies, setFromCurrencies] = useState([]);
const [selectedCurrencyState, setSelectedCurrencyState] = useState({ selectedToCurrency: null, selectedFromCurrency: null });
const [transactionType, setTransactionType] = useState(TRANSACTION_TYPES.BUY);
useEffect(() => {
getSwapPairs()
.then((res) => {
setSwapInfo(res.data);
setFromCurrencies(res.data.map(item => item.quoteAsset));
if (transactionType === 'BUY') {
setSelectedCurrencyState({ ...selectedCurrencyState, selectedFromCurrency: localStorage.getItem('fromCurrency') || 'USD', selectedToCurrency: symbol || localStorage.getItem('toCurrency') });
}
setTransactionType(localStorage.getItem('transactionType', transactionType) || TRANSACTION_TYPES.BUY);
}, []);
const handleInvertSelectedAssets = (fromCurrency, toCurrency) => {
setSelectedCurrencyState({ ...selectedCurrencyState, selectedFromCurrency: toCurrency, selectedToCurrency: fromCurrency });
localStorage.setItem('toCurrency', fromCurrency);
localStorage.setItem('fromCurrency', toCurrency);
};
Here is where I switch the transactionType:
const handleTransactionTypeChange = (type) => {
if (transactionType !== type) {
setTransactionType(type);
handleInvertSelectedAssets(selectedCurrencyState.selectedFromCurrency, selectedCurrencyState.selectedToCurrency);
localStorage.setItem('transactionType', type);
setAmountState({ toCurrencyAmount: '', fromCurrencyAmount: '' });
}
};
Feel like your problem lies in the way you update your state.
Maybe there is a typo on the updated selectedCurrencyState.selectedFromCurrency or maybe swapInfo is somehow updated in an invalid way ?
Could you fill in more info about how your state is managed ?
When you update state in React,
The following should not be done
this.state = {a:"red", b:"blue"};
if( this.state.b == "blue" ) {
this.setState({a: "green" });
}
Instead, do
this.setState((prevState)=>(
{a: (prevState.b=="blue")? "green": "red" }
));
In other words, state updates that depend on its previous value, should use the callback argument of the setState.
Eg: selectedCurrencyState depends on transactionType.
You can store both (or all state properties) as a single object.

React setState Array Hook doesn't re-render component [duplicate]

I want to update value of one object only but updating value of one Object, Updates the value for all objects.
let default = {
name: '',
age: ''
}
this.state = {
values: Array(2).fill(default)
}
updateName (event) {
let index = event.target.id,
values = this.state.values;
values[index].name = event.target.value;
this.setState ({
values: values
});
}
There are four significant problems in that code.
You're using the same object for all entries in your array. If you want to have different objects, you have to create multiple copies of the default.
You're calling setState incorrectly. Any time you're setting state based on existing state (and you're setting values based, indirectly, on this.state.values), you must use the function callback version of setState. More: State Updates May Be Asynchronous
You can't directly modify the object held in this.state.values; instead, you must make a copy of the object and modify that. More: Do Not Modify State Directly
default is a keyword, you can't use it as an identifier. Let's use defaultValue instead.
Here's one way you can address all four (see comments):
// #4 - `default` is a keyword
let defaultValue = {
name: '',
age: ''
};
this.state = {
// #1 - copy default, don't use it directly
values: [
Object.assign({}, defaultValue),
Object.assign({}, defaultValue),
] // <=== Side note - no ; here!
};
// ....
updateName(event) {
// Grab the name for later use
const name = event.target.value;
// Grab the index -- I __don't__ recommend using indexed updates like this;
// instead, use an object property you can search for in the array in case
// the order changes (but I haven't done that in this code).
const index = event.target.id;
// #2 - state updates working from current state MUST use
// the function callback version of setState
this.setState(prevState => {
// #3 - don't modify state directly - copy the array...
const values = prevState.values.slice();
// ...and the object, doing the update; again, I wouldn't use an index from
// the `id` property here, I'd find it in the `values` array wherever it
// is _now_ instead (it may have moved).
values[index] = {...values[index], name};
return {values};
});
}
Note that this line in the above:
values[index] = {...values[index], name};
...uses property spread syntax added in ES2018 (and shorthand property syntax, just name instead of name: name).
I would use the Array.prototype.map function with combination of the object spread syntax (stage 4):
Note that i changed the name of the default object to obj.
default is a reserved key word in javascript
let obj = {
name: '',
age: ''
}
this.state = {
values: Array(2).fill(obj)
}
updateName(event){
const {id, value} = event.target;
this.setState(prev => {
const {values} = prev;
const nextState = values.map((o,idx) => {
if(idx !== id)
return o; // not our object, return as is
return{
...o,
name: value;
}
});
return{
values: nextState
}
});
}
There is an easy and safe way to achieve that through the following:
this.setState({
values: [ newObject, ...this.state.values],
});
this will create an instance of the state and change the value of an existing object with new object.

Why react state (useState) is updated but not updated when log it?

Hi am trying to create a simple multi file component, But the files state is not behaving as expected.
The FileUpload component works fine and when the user chooses a file and it starts uploading the onStart prop method is called. And then when it finishes successfully the 'onFinish' prop method is called. This code seems fine to the point of consoling the object in the onfinish method. it consoles an old value of the object before it was modified by the onStart Method. I expected the file object value in the console to include the buffer key since it was added when the onStart method was called but it's not there.
Example initial state files should be [] when the use effect is called on the state files should be updated to [{_id:"example_unique_id"}] then a button for upload will appear and when user chooses a file and onStart modifies the object and the state should be updated to [{_id:"example_unique_id", buffer:{}] and finally when it finishes files should be [{_id:"example_unique_id", buffer:{}] but instead here it returns [{_id:"example_unique_id"}].
What could I be missing out on?
Also, I have React Dev tools installed and it seems the state is updated well in the dev tools.
import React, { useState } from 'react'
import { useEffect } from 'react';
import unique_id from 'uniqid'
import FileUpload from "./../../components/FileUpload";
const InlineFileUpload = ({ onFilesChange }) => {
const [files, setFiles] = useState([]);
function onFinish(file, id) {
const old_object = files.filter((file) => file._id == id)[0];
console.log("old object on after upload", old_object);
}
const addFile = (file, id) => {
const old_object = files.filter((file) => file._id == id)[0];
const index = files.indexOf(old_object);
const new_files = [...files];
new_files.splice(index, 1, { ...old_object, buffer: file });
setFiles(new_files);
};
useEffect(() => {
const new_attachments = files.filter(({ buffer }) => buffer == undefined);
if (new_attachments.length == 0) {
setFiles([...files, { _id: unique_id() }]);
}
const links = files.filter((file) => file.file !== undefined);
if (links.length !== 0) {
onFilesChange(links);
}
}, [files]);
return (
<>
{files.map((file) => {
const { _id } = file;
return ( <FileUpload
key={_id}
id={_id}
onStart={(e) => addFile(e, _id)}
onFinish={(e) => onFinish(e, _id)}
/>
);
})}
</>
);
};
export default InlineFileUpload
I think the problem is caused by the fact that your this code is not updating the state:
const addFile = (file, id) => {
const old_object = files.filter((file) => file._id == id)[0];
const index = files.indexOf(old_object);
const new_files = [...files];
new_files.splice(index, 1, { ...old_object, buffer: file });
setFiles(new_files);
}
files looks like an array of objects.
Spread operator will not do a deep copy of this array. There are a lot of examples on the internet, here is one.
let newArr = [{a : 1, b : 2},
{x : 1, y : 2},
{p: 1, q: 2}];
let arr = [...newArr];
arr[0]['a'] = 22;
console.log(arr);
console.log(newArr);
So your new_files is the same array. Splice must be making some modifications but that is in place. So when you are doing this setFiles(new_files);, you are basically setting the same reference of object as your newState. React will not detect a change, and nothing gets updated.
You have the option to implement a deep copy method for your specific code or use lodash cloneDeep.
Looking at your code, this might work for you : const new_files = JSON.parse(JSON.stringify(files)). It is a little slow, and you might lose out on properties which have values such as functions or symbols. Read
The reason you are getting the old log is because of closures.
When you do setFiles(new_files) inside addFiles function. React updates the state asynchronously, but the new state is available on next render.
The onFinish function that will be called is still from the first render, referencing files of the that render. The new render has the reference to the updated files, so next time when you log again, you will be getting the correct value.
If it's just about logging, wrap it in a useEffect hook,
useEffect(() => {
console.log(files)
}, [files);
If it's about using it in the onFinish handler, there are answers which explore these option.

Do I need to use the spread operator when using useState hook on object when updating?

I just started learning about hooks, and according to the official docs on Using Multiple State Variables, we find the following line:
However, unlike this.setState in a class, updating a state variable always replaces it instead of merging it.
So, if I understand correctly, this mean I don't need to use the spread operator for updating the state?
You still don't want to mutate state. So if your state is an object, you'll want to create a new object and set with that. This may involve spreading the old state. For example:
const [person, setPerson] = useState({ name: 'alice', age: 30 });
const onClick = () => {
// Do this:
setPerson(prevPerson => {
return {
...prevPerson,
age: prevPerson.age + 1
}
})
// Not this:
//setPerson(prevPerson => {
// prevPerson.age++;
// return prevPerson;
//});
}
That said, using hooks you often no longer need your state to be an object, and can instead use useState multiple times. If you're not using objects or arrays, then copying is not needed, so spreading is also not needed.
const [name, setName] = useState('alice');
const [age, setAge] = useState(30);
const onClick = () => {
setAge(prevAge => prevAge + 1);
}
What it means is that if you define a state variable like this:
const [myThings, changeMyThings] = useState({cats: 'yes', strings: 'yellow', pizza: true })
Then you do something like changeMyThings({ cats: 'no' }), the resulting state object will just be { cats: 'no' }. The new value is not merged into the old one, it is just replaced. If you want to maintain the whole state object, you would want to use the spread operator:
changeMyThings({ ...myThings, cats: 'no' })
This would give you your original state object and only update the one thing you changed.

How to call setState in react with nested objects and unknown key

My state looks like this
state = {
basic: {
entry: 'index.js',
output: {
path: 'dist',
filename: 'bundle.js',
}
}
}
I have defined a callback for input onChange event :
handleUpdateString = (e) => {
const name = e.target.name
const value = e.target.value
this.setState({ [name]: value })
console.log(this.state)
}
say my input name is 'basic.entry'
my state is updated, but instead of having this.state.basic be { entry: 'some value'}
I end up with a new key in my state : { basic.entry: 'some value' }
I have tried using dot-object to convert the dot-seperated string to a nested object, and passing this to setState but the state appears to be unchanged.
What are simple solutions to this problem ?
Use a switch statement with explicit setState.
This is not a React question but a pure JavaScript question on how to set a nested property on any object using a dot-notated string for the property. There is an answer here: Javascript object key value coding. Dynamically setting a nested value
Using that example in React:
handleUpdateString = (e) => {
const name = e.target.name
const value = e.target.value
const state = Object.assign({}, this.state);
setData(name, value, state);
this.setState(state);
console.log(this.state)
}

Categories

Resources