React - useState appending to the array changes previously appended objects - reactjs

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.

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.

Adding an item to array in React fails when using sockets

I've an array in React (using hooks), and I'm using socket.io to get notified on new items.
I've defined the Socket listener on the initializing useEffect, I'm getting notified when new objects should be added, but on this listener handler -
The current array is always empty
When I add the new object to the beginning of the array (using the spread operator) - the array will hold only the new object.
Any idea why?
function RecommendationsFeed() {
const [recommendations, setRecommendations] = useState<Recommendation[]>([]);
useEffect(() => {
const socket = socketIOClient(SOCKET_URL);
socket.on('recommendation', (newRecommendation: Recommendation) => {
newRecommendation.id = Math.floor(Math.random() * 10000) + 1;
const mergedRecommendationsList = [newRecommendation, ...recommendations];
setRecommendations(mergedRecommendationsList);
});
}, []);
return (
<div>
{ recommendations.map( currRec => <RecommendationFeedItem rec={currRec} key={currRec.id}/> ) }
</div>
)
}
I've also tried to add the recommendations state to the parameter list of the useEffect, but then it caused the listener to get processed multiple times.
useEffect(() => {
...
}, [recommendations]);
I found the issue.
Referring to the existing recommendations array in the set did the trick - by changing the update to:
setRecommendations((recommendations) => [newRecommendation, ...recommendations]);

useState non-instantaneous updates break function

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}
});
}

How to update to an array if element already exists

I'm making a React-Native application. Thanks to everyone's help I could somehow make that work except for toggling YES and NO. Once the user clicks on a button I just want to check if that clicked item data already exists in the state, if so I want to update it. If it does not exist then it should be added to the state as a new Item.
I already coded the above logic, my code is working, but not returning correct output, elements are not adding or updating to the state array properly. How do I fix this code?
I want to generate output like this
[{
chelistid:231,
validiary : "YES",
remark : "Hello"
},{
chelistid:232,
validiary : "NO",
remark : "asddddd"
}]
My code
const [reqData, setReqData] = useState([]);
//Modify yes clicked
const yesClicked = (element) => {
let req = {
"chelistid": element.chelistid,
"validiary": "Yes",
"remark": element.remark
}
createCheckList(req);
}
//Modify no clicked
const noClicked = (element) => {
let req = {
"chelistid": element.chelistid,
"validiary": "No",
"remark": element.remark
}
createCheckList(req);
}
const createCheckList = (data) => {
const index = reqData.findIndex(x => x.chelistid === data.chelistid)
var modifiedArray = reqData
if (index !== -1) {
//Remove the element from the array
modifiedArray.splice(index, 1);
}
setReqData([modifiedArray, data]);
}
The problem is it seems like you are not spreading the array to append the data element. What you are doing by [modifiedArray, data] you are creating an array that contains an array and data something like [[modified array content here], data]. But actually, you want to append to modified array instead. For that, you need to expand the modified array by using ... which is called spread syntax. (Learn here) So, your code would look like:
setReqData([...modifiedArray, data]);

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