Adding an item to array in React fails when using sockets - arrays

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

Related

How to avoid Maximum update depth exceeded Reactjs

I tried to display some csv content in reactjs application using below code. when i click the button it should call the api and get the response.i need to show the response in table formate and when i submit i need to save the data. Getting response and Saving is working fine for me but my issue is when i click the side menu to for fetch the selected data my state not updating properly to display the selected value data i need to click more than three times. so i tried with useeffect its with dependency its wokring fine . but its showing memory issue. i am new to react i tried many ways its showing maximum exceed error. so how to fix it in below code
const Parameters = ({ selectedtestcaseId, hideTestTab }) => {
const [csvattachment, setcsvattachment] = useState({})
const [slectedattachementId, setslectedattachementId] = useState()
const [viewcsv, setviewcsv] = useState()
const [csvHead, setcsvHead] = useState([])
const [csvformFields, setcsvformFields] = useState([{}])
const [formatData, setformatData] = useState([])
const formatCsv = () => {
console.log(csvHead)
if (viewcsv) {
let splitVlaues = viewcsv?.split('\r\n')
setcsvHead(splitVlaues[0]?.split(','))
let allData = []
for (let i = 1; i < splitVlaues.length - 1; i++) {
let a = splitVlaues[i].split('"');
let filteredValue = a.filter(x => x && x != ',');
allData.push(filteredValue)
}
setformatData(allData)
var newval = formatData.map(val => {
return val.reduce((result, field, index) => {
result[csvHead[index]] = field;
return result;
}, {})
})
setcsvformFields(newval)
}
}
useEffect(() => {
formatCsv()
}, [csvformFields])
return (
<table className="table table-striped table-bordered parameter-display">
<thead>
<tr>
{csvHead.map((val, i) => <td key={i}>{val}</td>)}
</tr>
</thead>
<tbody>
<>
{csvformFields?.map((d, index) => (
<tr>
{Object.keys(d).map(prop => (
<>
<td><input type="text" name={prop} value={d[prop]} onChange={(e) => handleFormChange(index, e)} /></td>
</>
))}
</tr>
))}
</>
</tbody>
</table>
)
}
My in input is
in variable "viewcsv" ie const [viewcsv, setviewcsv] = useState()
/
criteriass,criteriaCountry,criteriaCountryState
"5G,NewZealand","5G,New Zealand ","5G,New Zealand "
"5G,NewZealand","5G,New Zealand ","5G,New Zealand "
"5G,NewZealand","5G,New Zealand ","5G,New Zealand "
/
2) const [csvHead, setcsvHead] = useState([])
csvHead hold ['criteriass','criteriaCountry']
3) const [formatData, setformatData] = useState([])
formatData and allData hold
[
[
"5G,NewZealand",
"5G,New Zealand "
]
]
4) const [csvformFields, setcsvformFields] = useState([{}])
csvformFields and newval hold
[
{
"criteriass": "5G,NewZealand",
"criteriaCountry": "5G,New Zealand "
}
]
My issue is after i try to use csvformFields in my html its not loading
after use useEffeect with csvformFields as dependency its working. but throwing infinite loop.
How to fix the above. How to make state change reflect immeditaly
I tried to call "csvformFields" using useEffect its thrwoing maximum exceed error and also i tried with "csvHead " varibale still the same.. How can i fix the maxmium exceed error in the above code . where i am making the mistake. which variable i need to add as dependency array in useEffect and why??
below is the error which i am getting
*
Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render
*
The issue comes from your useEffect
useEffect(() => {
formatCsv()
}, [csvformFields])
Here you call out a function called formatCsv whenever the value of csvformFields changes. Inside the formatCsv function you call out setcsvformFields that changes the value of csvformFields. That creates an infinite loop:
useEffect calls formatCsv -> formatCsv changes csvformFields value -> useEffect dependency array trigers and calls the formatCsv function -> formatCsv changes csvformFields value -> so on and so on.
The fix is to fix the dependency array or useEffect. The formatCsv only depend on 3 values - viewcsv formatData and csvHead. They what define csvformFields value. So dependency array should be set to those - not the csvformFields.
useEffect(() => {
formatCsv()
}, [viewcsv, formatData, csvHead])
BUT that wont solve the issue as the infinite loop will still persist. Its down to the flaw of formatCsv function. inside you have setState functions for setformatData and setcsvHead states. Why? I got no clue.
Setting a state and then in the function trying to get the newest value? that wont work. Once a function is executed the values of states inside the function cycle will remain the same no matter how much u change em. so just make them into a variable.
Overall you need to refactor the entire flow. The code is a mess - no proper formatting, dosent follow the standard programing namings, etc. + you HEAVILY rely on SOOO many different states its absurd.
tl;dr; the issue is by setting the wrong useEffect dependency, having too many states and a bad formatCsv function design.

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.

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

Unable to understand setState of react js function paramters in a specific call?

setListOfPosts(curPosts => {
let newPosts = [...curPosts];
newPosts[newPosts.findIndex(p => p.id === postId)].alert = response.data;
}
});
//is curPosts an instance of array or complete array?? my listofPosts is an array of objects
Your setState call needs to return newPosts, and you're creating an array using the spread operator which is why it's coming back as an array of objects.
I'm not sure what your desired output is, but by adding a return function it will set the state:
setListOfPosts(curPosts => {
let newPosts = [...curPosts];
newPosts[newPosts.findIndex(p => p.id === postId)].alert = response.data;
return newPosts
}
});
This is untested but if your logic is correct should return an array of objects with the objects alert value updated.
Another option would be to do your logic before your setState call, by creating a a newState array and then simply updating the state with that new array without the use of the callback.
The callback function is useful if you want to add a new object to state array or do something that preserves the initial state, in your example you could do it without the callback like this:
// Create a copy of the state array that you can manipulate
const newPosts = [...newPosts]
if (data.response) {
// Add your logic to the state copy
newPosts[newPosts.findIndex(p => p.id === postId)].alert = response.data;
// Replace state with state copy
setListOfPosts(newPosts)
}
Again untested but hopefully this should help you understand the use of the callback function and the right way to use 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