useState non-instantaneous updates break function - reactjs

I have a sumButtonsDict state variable that stores a dictionary of objects. I've also built a simple addSumButton() function add a new object into the sumButtonsDict:
const [sumButtonsDict, setSumButtonsDict] = useState({})
function addSumButton(sum) {
const dictKey = Object.keys(sumButtonsDict).length
const sumButtonDict = {
sum: sum,
isActive: false
}
setSumButtonsDict(prevState => ({...prevState, [dictKey]: sumButtonDict}))
}
As you can see, the function stores every new dictionary item at a key corresponding to the index it's on (e.g., first item has key 0, second item has key 1...), but the correct key is derived from the count of objects already existing in sumButtonsDict.
When the component mounts, I add 5 new buttons using the following:
useEffect(() => {
addSumButton(10)
addSumButton(25)
addSumButton(50)
addSumButton(100)
addSumButton(250)
}, [])
but only 1 ends up existing in sumButtonsDict. I suspect this is because setState() doesn't update the state variable immediately, and hence when I call Object.keys(sumButtonsDict).length it keeps on returning 0 even though the addSumButton() has run multiple times before.
How can I get around this?

You're already using the function version of setSumButtonsDict to get the previous state, which is the right thing to do. You just need to move the other bits of code into the function too, so the entire calculation uses prevState:
function addSumButton(sum) {
setSumButtonsDict(prevState => {
const dictKey = Object.keys(prevState).length;
const sumButtonDict = {
sum: sum,
active: false,
}
return {...prevState, [dictKey]: sumButtonDict}
});
}

Related

Redux-form fires an extra CHANGE action when deleting an element in complex fieldArray

I have a form built with reduxForm which has a nested fieldArray component.
In this component, I have two dropdowns. The first determines what will be in the second dropdown.
Here's my solution for that
// Set initial list values when hydrating form
const setInitialQualifiantTypeList = () => {
const list: (string | undefined)[] = []
fields.forEach((qualifiant: any, index: number) => {
const qualifiantType = fields.get(index).qualifiantType
list.push(qualifiantType ? `B0${qualifiantType}` : undefined)
})
return list
}
let qualifiantTypeList = setInitialQualifiantTypeList()
// Change list parameter when changing the type
const onChangeQualifiantType = (
qualifiantType: string,
qualifiant: string,
name: string,
index: number
) => {
let newQualifiantTypeList = [...qualifiantTypeList]
if (qualifiantType === '') {
newQualifiantTypeList[index] = undefined
} else {
newQualifiantTypeList[index] = `B0${qualifiantType}`
// setQualifiantTypeList(newQualifiantTypeList)
qualifiantTypeList = newQualifiantTypeList
}
// If it doesn't match previous type reset the value
if (qualifiantType !== qualifiant.substring(0, 2)) {
resetValue(`${name}[${index}].qualifiant`)
}
}
qualifiantTypeList gets passed as a prop to my dropdown component which gets the appropriate list.
However, when it comes to the fieldArray part, I lose some data if deleting any of my fields except the last. I've tried fields.remove(index) and even change(form, field, newArray) replacing the entire value, but redux-form fires an extra CHANGE no matter what, making my data go from
[
{
"qualifiantType": "94",
"qualifiant": "1KBC-CA-Q"
},
{
"qualifiantType": "95",
"qualifiant": "2A"
},
{
"qualifiantType": "96",
"qualifiant": "3B"
}
]
to
[
{
"qualifiantType": "94",
"qualifiant": "1KBC-CA-Q"
},
{
"qualifiantType": "96"
}
]
The state I'm looking for does appear in the redux devtools, but an extra CHANGE action gets dispatched and clears it.
I've logged my onChangeQualifiantType function and it's not firing during this process, so that's not the cause.
How I can I prevent this behavior so as to simply delete the array element and keep everything else?
After some digging, turns out it's because the sub-list is an async DB fetch, it renders first with the new answer, the component clears it because it's a dropdown and the value is replaced by the time the data is in place.

ReactJS map: Expected an assignment or function call and instead saw an expression - no-unused-expressions

I'm in the midst of cleaning up errors for a repo and I've come across this error where someone's trying to to assign a tag value object to a const variable inside of a map function. Here's its current form:
const BatchEditState = {
CURRENT: 'CURRENT',
DELETE: 'DELETE',
PUT: 'PUT',
}
handleShow = () => {
this.batchEditSet = {};
this.state.currentTags.map((tag) => {
this.batchEditSet[tag.tag_name] = BatchEditState.CURRENT;
});
};
As far as I've researched, one is definitely not supposed to go about it this way even if it does still function. I've seen plenty examples returning a jsx element, but I'm pretty sure that's not the point for this. I do know a map function is supposed to at least return a value however.
I attempted to use a spread operator and an implicit return, but that didn't work out. I also tried making a basic return & even though I'm not encountering any immediate errors in our application, I'm still not sure if this is the right way to go. Still fairly new at this, but appreciate any info, help, and education I can get
handleShow = () => {
this.batchEditSet = {};
this.state.currentTags.map((tag) => {
this.batchEditSet[tag.tag_name] = BatchEditState.CURRENT;
return(
BatchEditState.CURRENT
)
});
};
.map is only for creating new arrays by iterating over an existing array. While you want to iterate over an existing array, you don't want to create a new array - rather, you want to construct a plain object - so .map should not be used here. (The array you're constructing in your current code is going unused.)
To procedurally assign to properties of the object, do:
handleShow = () => {
this.batchEditSet = {};
this.state.currentTags.forEach((tag) => {
this.batchEditSet[tag.tag_name] = BatchEditState.CURRENT;
});
};
Or create an array of entries, then turn that array into an object.
handleShow = () => {
this.batchEditSet = Object.fromEntries(
this.state.currentTags.map(tag => [tag.tag_name, BatchEditState.CURRENT])
);
};
But also, doing this.batchEditSet = in the first place looks like a mistake in React. If this is a component, you should almost certainly be calling this.setState instead of mutating the instance.

React - useState appending to the array changes previously appended objects

Hi all,
I have a small react app that is creating (mapping from an array) new tabs (and panels) when there is a new message over the websocket.
There is an initial setup, that is hardcoded for the test purposes which sets up 2 tabs on load, any new ones should be appended to these two.
const INITIAL_ARRAY= [
{
id: 1,
child_component_config: {...}
},
{
id: 2,
child_component_config: {...}
}
];
const template = {
child_component_config: {...}
}
The code, simplifed:
const [current_array, setNewArray] = useState( INITIAL_ARRAY);
export default function ParentComponent() {
useEffect(() => {
const client = new ws...
client.onConnect = function (frame) {
var message = clientDesk.subscribe( '/topic/desks/4', function (message) {
// on message
var new_tab = TEMPLATE;
new_tab.id = Math.max( ...current_array.map( elem => elem.id ) ) + 1;
setActiveTab(new_tab.id);
setNewArray([...current_array, new_tab]);
}
}
}, [ current_array ]);
const tabs = map tabs
cosnt panels = map panels
return(
{tabs}
{panels}
)
The problem:
On first message from the WS the third element is added to the array properly (example-1)
, fourth one is added properly but it also overwrites the third element (example-2)
making them exactly the same. After fourth it gets strange, where some are overwritten and some are not.
I've tried:
Moving the state updating out of useEffect or removing useEffect completly
Storing current_array in a temp var before any logic
Adding a counter to track which tab's id is the latest -> tracking state of just one number works
const [tab_count, setTabCount] = useState( INITIAL_ARRAY.lenght );
Using counter to try to force rendering
Setting up a fixed number of objects in the initial array and just update which ever is needed (with and without counter)
Updating based on the previous value
setNewArray( prevArray => {
logic
return [...prevArray, new_tab];
}
After the first WS message, if the code is changed/saved and webpack compiled, the next message will add a new element to the array properly.
EDIT - Solved:
Managed to solve this by building a new object (instead of using the template) before adding it to the array.

React Native - useState hook to update array within an object of arrays

How do I use useState hook to update an array within an array of objects that's dependent on an array index?
end goal data:
foodData = [
{
foodId: 'fdsafsdafsa',
fruitsArray: ['banana', 'orange']
},
{
foodId: '234243fdsfdsafsasdf343432afsdafsa',
fruitsArray: ['apple']
},
{
foodId: 'fdsafsdafsa',
fruitsArray: ['strawberry', 'orange']
},
]
I have a function with arguments, (fruits, fruitIndex, foodIndex, foodId)
const logFruitsIntoFoodData = (fruits, fruitIndex, foodIndex, foodId) => {
// update state here...
const foodToUpdate = {...foodData};
foodToUpdate[foodIndex] = {
...foodData[foodIndex],
// This gets overwritten,
// how do I continue to add or
// update the fruit based on fruit index?
['fruitsArray']: fruits,
};
}
I'm trying to update/add fruits into the fruitsArray so when the function gets invoked, it'll insert the proper fruits into foodData, or it'll update it, depending on what the fruitIndex is.
Should I be using two separate useState:
const [foodData, setFoodData]= useState([]);
const [fruitsArray, setFruitsArray] = useState([]);
where I should get the array of fruits first, then add it into foodData, or can I just have 1 useState to deal with everything?
I'm not sure I'd use an array, since you have foodId, meaning you could easily convert foodData into an object. I'd say if you're using an array then use the index. If you want to use foodId to identify the item you're updating, then just make foodData an object.
It's a little bit cumbersome but this is what I think you're trying to do:
const logFruitsIntoFoodData = (fruit, fruitIndex, foodIndex) => {
// isolate the food you're updating and clone it
const foodToUpdate = { ...foodData[foodIndex] };
// replace fruit in the specified index...
foodToUpdate.fruitsArray.splice(fruitIndex, 1, fruit);
// clone existing state
const newFoodDataState = [...foodData];
// replace the item in the specified
newFoodDataState.splice(foodIndex, 1, foodToUpdate);
// set new state
setFoodData(newFoodDataState);
};
I'm not 100% sure this is what you meant. Let me know and I can modify accordingly!
I was able to get it to work when I did it like this:
const [foodData, setFoodData]= useState([]);
const logFruitsIntoFoodData = (fruits, fruitIndex, foodIndex, foodId) => {
// checks if the index exists in the array.
if (!foodData[foodIndex]) {
foodData[foodIndex] = {
foodId,
fruitsArray: [],
};
setFoodData({...foodData});
}
// now I can insert fruits depending on the fruitIndex
foodId[foodIndex].fruitsArray[fruitIndex] = fruits;
}
Not sure if this is the best method, but it works. If anyone has a better way, I'd love to hear it!

Conditional dropdowns with react-select in react-final-form initialized from the state

I'm using react-select and react-final-form for conditional dropdowns, where options for the second select are provided by a <PickOptions/> component based on the value of the first select (thanks to this SO answer).
Here is the component:
/** Changes options and clears field B when field A changes */
const PickOptions = ({ a, b, optionsMap, children }) => {
const aField = useField(a, { subscription: { value: 1 } });
const bField = useField(b, { subscription: {} });
const aValue = aField.input.value.value;
const changeB = bField.input.onChange;
const [options, setOptions] = React.useState(optionsMap[aValue]);
React.useEffect(() => {
changeB(undefined); // clear B
setOptions(optionsMap[aValue]);
}, [aValue, changeB, optionsMap]);
return children(options || []);
};
It clears the second select when the value of the first one changes by changeB(undefined). I've also set the second select to the first option in an array by passing initialValue. As I need to initialize the values from the state, I ended up with the following code:
initialValue={
this.state.data.options[index] &&
this.state.data.options[index].secondOption
? this.state.data.options[index]
.secondOption
: options.filter(
option => option.type === "option"
)[0]
}
But it doesn't work. Initial values from the state are not being passed to the fields rendered by <PickOptions/>. If I delete changeB(undefined) from the component, the values are passed but then the input value of the second select is not updated, when the value of the first select changes (even though the options have been updated). Here is the link to my codesandbox.
How can I fix it?
I was able to get this to work by taking everything that is mapped by the fields.map() section and wrapping it in it's own component to ensure that each of them have separate states. Then I just put the changeB(undefined) function in the return call of the useEffect hook to clear the secondary selects after the user selects a different option for the first select like so:
React.useEffect(() => {
setOptions(optionsMap[aValue]);
return function cleanup() {
changeB(undefined) // clear B
};
}, [aValue, changeB, optionsMap]);
You can see how it works in this sandbox: React Final Form - Clear Secondary Selects.
To change the secondary select fields, you will need to pass an extra prop to PickOptions for the type of option the array corresponds to. I also subscribe and keep track of the previous bValue to check if it exists in the current bValueSet array. If it exists, we leave it alone, otherwise we update it with the first value in its corresponding optionType array.
// subscibe to keep track of previous bValue
const bFieldSubscription = useField(b, { subscription: { value: 1 } })
const bValue = bFieldSubscription.input.value.value
React.useEffect(() => {
setOptions(optionsMap[aValue]);
if (optionsMap[aValue]) {
// set of bValues defined in array
const bValueSet = optionsMap[aValue].filter(x => x.type === optionType);
// if the previous bValue does not exist in the current bValueSet then changeB
if (!bValueSet.some(x => x.value === bValue)) {
changeB(bValueSet[0]); // change B
}
}
}, [aValue, changeB, optionsMap]);
Here is the sandbox for that method: React Final Form - Update Secondary Selects.
I also changed your class component into a functional because it was easier for me to see and test what was going on but it this method should also work with your class component.
Based on the previous answer I ended up with the following code in my component:
// subscibe to keep track of aField has been changed
const aFieldSubscription = useField(a, { subscription: { dirty: 1 } });
React.useEffect(() => {
setOptions(optionsMap[aValue]);
if (optionsMap[aValue]) {
// set of bValues defined in array
const bValueSet = optionsMap[aValue].filter(x => x.type === optionType);
if (aFieldSubscription.meta.dirty) {
changeB(bValueSet[0]); // change B
}
}
}, [aValue, changeB, optionsMap]);
This way it checks whether the aField has been changed by the user, and if it's true it sets the value of the bField to the first option in an array.

Resources