Update array state from out of a for each loop - javascript

I'm trying to update the state of an array from out of a forEach loop without loosing the previous state. I`m trying to archive something like the following:
const initialState = [{question: "a", answer: ""}, {question: "b", answer: ""}]
const [request, setRequests] = useState(initialState);
const run = () => {
request.forEach((request, idx) => {
fetch("/ask").then(data => data.json()).then(response => {
let currentState = request;
request[idx] = Object.assign(...request[idx], {answer: response.answer});
setRequests(currentState);
})
})
}
But in such a case only one response will be rendered. Any idea how to archive something like this?

The Problems
There are a couple of issues there:
You're breaking one of the fundamental rules of React state: Do not modify state directly. Doing const currentState = request; doesn't copy the object, it just makes two variables point to the same object.
You're using the version of state setter where you just pass in the update, which completely overwrites any previous outstanding state updates.
Solutions
There are (at least) two approaches here:
Do piecemeal updates as you are in that code, where earlier updates are stored in state (causing a re-render) while later updates are still in progress
or
Get all the updated information, and do a single state update (and render)
Both are valid depending on your use case.
Piecemeal Updates
For the piecemeal updates, since you're doing a bunch of state updates (which may not occur in order, depending on the vagaries of the timing of the fetch replies). Since you're updating state based on previous state (the other entries in the array), you need to use the callback version of the state setter. Then, you need to create a new array each time, and and new object within the array for the object at index idx, like this:
// *** Note I've renamed `request` to `requests` -- it's an array, it should
// use the plural, and you use `request` for an individual one later.
// I've also reversed `response` and `data`, since they were backward in the
// original. I've also added (minimal) handling of errors.
const [requests, setRequests] = useState(initialState);
const run = () => {
requests.forEach((request, idx) => {
// (It seems odd that nothing from `request` is used in the `fetch`)
fetch("/ask")
.then((response) => response.json())
.then((data) => {
setRequests((prevRequests) =>
prevRequests.map((request, index) => {
return idx === index ? { ...request, answer: data.answer } : request;
})
);
})
.catch((error) => {
// ...handle/report error...
});
});
};
map creates the new array, returning a new object for the one at index idx, or the previous unchanged ones for the others.
All At Once
The other approach is to do all the fetch calls, wait for them all to complete, and then do a single state update, like this:
// (Same naming and error-handling updates)
const run = () => {
Promise.all(requests.map((request) =>
fetch("/ask")
.then((response) => response.json())
.then((data) => {
return {...request, answer: data.answer};
})
))
.then(setRequests)
.catch((error) => {
// ...handle/report error...
});
};

You can build the new array at once and update it, something like this
const run = () => {
Promise.all(request.map((r) => fetch("/ask").then(data => data.json())))
.then((responses) => {
const newRequest = responses.map(res => ({answer: res.answer}))
setRequests(newRequest);
})
}
Using Promise.all allows you to retrieve all the json response at once, so you can build the new state in one shot.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
note: code not tested.

You problem stems from the fact that let currentState = request will keep a reference to the request at the time of making the closure. This means that it won't get updated by future calls.
You need to use the functional version of setState. Something like this:
const initialState = [{question: "a", answer: ""}, {question: "b", answer: ""}]
const [request, setRequests] = useState(initialState);
const run = () => {
request.forEach((request, idx) => {
fetch("/ask").then(data => data.json()).then(response => {
setRequests(currentState => {
currentState[request[idx]] = {answer: response.answer};
return {...currentState};
});
})
})
}

Related

React: How to update a state prior to another function?

I have a state variable called list that updates when setList is called. SetList lives under the function AddToList, which adds a value to the existing values in the list. As of this moment, the function handleList executes prior to the state variable setList even though I have setList added prior to the function handleList. What I am trying to achieve is for the setList to update its list prior to running the handleList. Could you provide insights on how to fix this?
If you want to test the code, https://codesandbox.io/s/asynchronous-test-mp2fq?file=/Form.js
export default function Form() {
const [list, setList] = useState([]);
const addToList = (name) => {
let newDataList = list.concat(name);
setList(newDataList);
console.log("List: ", list);
handleList();
};
const handleList = async () => {
console.log("Handle List Triggered");
await axios
// .put("", list)
.get("https://api.publicapis.org/entries")
.then((response) => {
console.log("Response: ", response);
})
.catch((error) => {});
};
return (
<AutoComplete
name="list"
label="Add to List"
onChange={(events, values) => {
addToList(values.title);
}}
/>
);
}
As you can tell, the get response is made prior to updating the list.
It's not clear what you want to do with the updated list, but you know what the new list will be, so you can just pass that around if you need it immediately.
const addToList = (name) => {
let newDataList = list.concat(name);
setList(newDataList);
console.log("List: ", list);
handleList(newDataList);
};
const handleList = async (list) => {
console.log("Handle List Triggered");
await axios
// .put("", list)
.get("https://api.publicapis.org/entries")
.then((response) => {
console.log("Response: ", response);
})
.catch((error) => {});
};
React's useEffect hook has an array of dependencies that it watches for changes. While a lot of the time it's used with no dependencies (i.e. no second parameter or empty array) to replicate the functionality of componentDidMount and componentDidUpdate, you may be able to use that to trigger handleList by specifying list as a dependency like this:
useEffect(() => {
handleList(list);
}, [list]);
I think there may be a redundant request when page loads though because list will be populated which you'll most likely want to account for to prevent unnecessary requests.
first of all you have to understand setState is not synchronized that means when you call setList(newDataList) that not gonna triggered refer why setState is async
therefore you can use #spender solution
or useStateCallback hook but it's important understand setState is not sync
const [state, setState] = useStateCallback([]);
const addToList = (name) => {
........... your code
setList(newDataList, () => {
// call handleList function here
handleList();
});
}

I have an array state of my react component, should I use Object.assign() when I add new item? I tried spread operator but see strange behavior

I have a functional react component which receives a random message object regularly. It displays the new added item properly. After I delete one item and received a new message, deleted item remains again. I solved by using Object.assign(), but not sure why spread operator is not working.
I think spread operator copies array. I have been using spread operator when adding new item to array state variable. When to use Object.assign(), when to use spread operator?
Here is my code.
const myComp = props => {
const [messages, setMessages] = useState([]);
const [api] = useState(new Api({
messageCallback: (msg) => {
handleCallback(msg);
},
}));
useEffect(()=>{
api.start();
},[])
const handleCallback = msg => {
messages.push(msg);
setMessages([...messages]);
}
const deleteItem = index => {
messages.splice(index,1);
setMessages([...messages]);
}
return (
<div>
{
messages.map((item, index)=>(
<p key={index} onClick={(e)=>deleteItem(index)}>{item.message}</p>
))
}
</div>
)
}
This works not properly so I solved by using this.
const handleCallback = msg => {
const temp = Object.assign(messages,[]);
temp.push(msg);
setMessages(temp)
}
Your existing code has a number of problems:
The API initialized on mount with
const [api] = useState(new Api({
messageCallback: (msg) => {
handleCallback(msg);
},
}));
sees the handleCallback declared on mount - and that function only has scope of the messages that exists on mount, which is an empty array.
You're mutating the existing state when you do
messages.push(msg);
Instead, clone the state before changing. Same for
messages.splice(index,1);
To fix all of these, change:
const handleCallback = msg => {
messages.push(msg);
setMessages([...messages]);
}
const deleteItem = index => {
messages.splice(index,1);
setMessages([...messages]);
}
to
const handleCallback = msg => {
setMesesages(
// use the callback form so that the `messages` argument
// refers to the most up-to-date state, not the state on mount
messages => {
const newMessages = [...messages]; // create shallow copy of state
// so you don't mutate the existing state
newMessages.push(msg);
return newMessages;
}
);
};
const deleteItem = index => {
setMesesages(
// use the callback form so that the `messages` argument
// refers to the most up-to-date state, not the state on mount
messages => {
const newMessages = [...messages]; // create shallow copy of state
// so you don't mutate the existing state
newMessages.splice(index, 1);
return newMessages;
}
);
};
Since you probably don't want to call new Api more than once, I'd recommend either using the functional version of useState (which gets called once, on mount):
const [api] = useState(() => new Api({
messageCallback: (msg) => {
handleCallback(msg);
},
}));
Or create the API in an effect hook instead. Since the API doesn't look to be used anywhere else in the component, it doesn't need to be stateful at all, I think.
useEffect(() => {
const api = new Api({
messageCallback: (msg) => {
handleCallback(msg);
},
});
api.start();
}, []);
When to use Object.assign()
Generally, when you need to make a shallow copy of an object (almost always not an array), eg
const shallowCopy = Object.assign({}, someObj);
This is identical to using spread syntax in an object:
const shallowCopy = { ...someObj };
You can also use spread syntax when you want to create a shallow copy of an array, like with
const newMessages = [...messages];
Operations like this mutate the array in place, and should be avoided with state values as there could be unexpected side-effects:
messages.push(msg);
Instead, create the new array when setting the state:
const handleCallback = msg => {
setMessages([...messages, msg]);
}
The same can be done when removing an element:
const deleteItem = index => {
setMessages([...messages.filter((m, i) => i !== index)]);
}
The problem is not about spreading but how you set your states.
You are supposed to set the state using setState, i.e. setMessages instead of mutating it (messages) directly like what you did here:
const handleCallback = msg => {
messages.push(msg); //here you try to mutate the state messages by .push
setMessages([...messages]);
}
const deleteItem = index => {
messages.splice(index,1); //here you try to mutate the state messages by .splice
setMessages([...messages]);
}
Changing these 2 functions as below is one of the ways to make your code work:
const handleCallback = msg => {
setMessages([...messages, msg]);
}
const deleteItem = index => {
setMessages([...messages.filter((v,i) => i !== index)]); //note that .filter does not mutate "messages" but creates a new array
}
In general, many developers are adding a new item by the DOM event. In my app, it is automatically added by the API without rendering the UI. That was the problem.
https://thecodebarbarian.com/object-assign-vs-object-spread.html
Spread operator also copies the array but have different value though original value changes. When you use Object.assign, copied value has the same value when original value changes.
In my app, when I use spread operator, message variable remains the original empty value though it seems to change.

React stale state in useEffect when using empty dependency array

I'm trying to read state after it's modified within useEffect hook. However since useEffect closes over availability being empty array, it's not updated at all causing infinite loop.
I'm intentionally using empty dependency array since I want this to happen only after
the first render, so including availability as dependency is not an option.
I need some way to read up-to-date value from availability but I don't know how to.
const [ availability, setAvailability ] = useState([])
const updateAvailability = (data, manufacturer) => {
setAvailability(availability => {
const copy = {...availability}
copy[manufacturer] = data
return copy
})
}
const parseAvailability = products => {
products.forEach(product => {
if (!availability[product.manufacturer]) { // availability is stale after updates
updateAvailability({}, product.manufacturer) // notify that data is coming soon
service.getAvailability(product.manufacturer).then(data => { // http request to fetch availability
updateAvailability(data, product.manufacturer)
})
}
})
}
useEffect(() => {
config.categories.forEach(category => {
service.getCategory(category.name).then(data => {
parseAvailability(data, availRef)
})
})
},[])

React re-render not showing updated state array

I am updating state(adding new object to state array) in event handler function.
const handleIncomingData = (data) => {
setState(state => {
var _state = state
if (_state.someDummyStateValue === 1234) {
_state.arr.push({ message: data.message })
}
return _state
})
}
React.useEffect(() => {
socket.on('data', handleIncomingData)
return()=>{
socket.off('data', handleIncomingData)
}
},[])
I am updating state in this manner as event handler function is not having access to latest state values. console.log(state) shows updated state but after re-render newly added object is not displayed. Also when same event fires again previously received data is displayed but not the latest one. Is there something wrong with updating state in this manner?
Object in javascript are copied by reference (the address in the memory where its stored).
When you do some thing like:
let obj1 = {}
let obj2 = obj1;
obj2 has the same reference as obj1.
In your case, you are directly copying the state object. When you call setState, it triggers a re-render. React will not bother updating if the value from the previous render is the same as the current render. Therefore, you need to create a new copy of your state.
Try this instead:
setState(prevValue => [...prevValue, {message: data.message}])
To better illustrate my point here's a codesandbox: https://codesandbox.io/s/wild-snowflake-vm1o4?file=/src/App.js
Try using the default way of making states it is preferred, clean and simple codeing way
const [message, setMessage] = useState([])
const handleIncomingData = data => {
let newMessage = message;
if (data.message.somevalue === 1234){
newMessage.push(data.message);
setMessage(newMessage)
}
}
React.useEffect(() => {
socket.on('data', handleIncomingData)
return()=>{
socket.off('data', handleIncomingData)
}
},[message])
Yes, it's wrong... You should never do operations directly in the state...
If you want to append messages to your array of messages, the correct snippet code would be:
const [messages, setMessage] = useState([])
const handleIncomingData = useCallback(
data => {
if (messages.someDummyStateValue === 1234) {
setMessage([...messages,{message:data.message}])
}
// do nothing
},[messages])
React.useEffect(() => {
socket.on('data', handleIncomingData)
return()=>{
socket.off('data', handleIncomingData)
}
},[handleIncomingData])
This way I'm not doing any operation to any state, I'm just replace the old state, with the new one (which is just the old one with a new message)
Always you want to manipulate data in an array state, you should never do operations directly in the state...

Setting a useEffect hook's dependency within`useEffect` without triggering useEffect

Edit: It just occurred to me that there's likely no need to reset the variable within the useEffect hook. In fact, stateTheCausesUseEffectToBeInvoked's actual value is likely inconsequential. It is, for all intents and purposes, simply a way of triggering useEffect.
Let's say I have a functional React component whose state I initialize using the useEffect hook. I make a call to a service. I retrieve some data. I commit that data to state. Cool. Now, let's say I, at a later time, interact with the same service, except that this time, rather than simply retrieving a list of results, I CREATE or DELETE a single result item, thus modifying the entire result set. I now wish to retrieve an updated copy of the list of data I retrieved earlier. At this point, I'd like to again trigger the useEffect hook I used to initialize my component's state, because I want to re-render the list, this time accounting for the newly-created result item.
​
const myComponent = () => {
const [items, setItems] = ([])
useEffect(() => {
const getSomeData = async () => {
try {
const response = await callToSomeService()
setItems(response.data)
setStateThatCausesUseEffectToBeInvoked(false)
} catch (error) {
// Handle error
console.log(error)
}
}
}, [stateThatCausesUseEffectToBeInvoked])
const createNewItem = async () => {
try {
const response = await callToSomeService()
setStateThatCausesUseEffectToBeInvoked(true)
} catch (error) {
// Handle error
console.log(error)
}
}
}
​
I hope the above makes sense.
​
The thing is that I want to reset stateThatCausesUseEffectToBeInvoked to false WITHOUT forcing a re-render. (Currently, I end up calling the service twice--once for win stateThatCausesUseEffectToBeInvoked is set to true then again when it is reset to false within the context of the useEffect hook. This variable exists solely for the purpose of triggering useEffect and sparing me the need to elsewhere make the selfsame service request that I make within useEffect.
​
Does anyone know how this might be accomplished?
There are a few things you could do to achieve a behavior similar to what you described:
Change stateThatCausesUseEffectToBeInvoked to a number
If you change stateThatCausesUseEffectToBeInvoked to a number, you don't need to reset it after use and can just keep incrementing it to trigger the effect.
useEffect(() => {
// ...
}, [stateThatCausesUseEffectToBeInvoked]);
setStateThatCausesUseEffectToBeInvoked(n => n+1); // Trigger useEffect
Add a condition to the useEffect
Instead of actually changing any logic outside, you could just adjust your useEffect-body to only run if stateThatCausesUseEffectToBeInvoked is true.
This will still trigger the useEffect but jump right out and not cause any unnecessary requests or rerenders.
useEffect(() => {
if (stateThatCausesUseEffectToBeInvoked === true) {
// ...
}
}, [stateThatCausesUseEffectToBeInvoked]);
Assuming that 1) by const [items, setItems] = ([]) you mean const [items, setItems] = useState([]), and 2) that you simply want to reflect the latest data after a call to the API:
When the state of the component is updated, it re-renders on it's own. No need for stateThatCausesUseEffectToBeInvoked:
const myComponent = () => {
const [ items, setItems ] = useState( [] )
const getSomeData = async () => {
try {
const response = await callToSomeService1()
// When response (data) is received, state is updated (setItems)
// When state is updated, the component re-renders on its own
setItems( response.data )
} catch ( error ) {
console.log( error )
}
}
useEffect( () => {
// Call the GET function once ititially, to populate the state (items)
getSomeData()
// use [] to run this only on component mount (initially)
}, [] )
const createNewItem = async () => {
try {
const response = await callToSomeService2()
// Call the POST function to create the item
// When response is received (e.g. is OK), call the GET function
// to ask for all items again.
getSomeData()
} catch ( error ) {
console.log( error )
}
} }
However, instead of getting all items after every action, you could change your array locally, so if the create (POST) response.data is the newly created item, you can add it to items (create a new array that includes it).

Categories