I'm a ReactJS notive and while related questions on this topic have been asked, but I couldn't find the answer I'm looking for.
In ReactJS, I have two state variables. When one changes (let's call it A), I want the other (B) to change. My code currently does this correctly; when I drop breakpoints or log to console, B changes correctly when A changes. However, React does not render the updated B until A changes again. What is the cause, and what is the right React pattern to ensure B renders?
Snippets of my code (happy to answer more)
This is my variable A:
const [prompt, setPrompt] = useState(params.prompt);
This is my variable B:
let defaultPromptsResultsArray = [{
isLoading: true,
prompt: params.prompt,
counter: 0,
URIs: [default_uri]
}]
const [promptsResultsArray, setPromptsResultsArray] = useState(defaultPromptsResultsArray);
This is the useEffect that depends on prompt (my state variable A):
useEffect(() => {
// Take 0 for the newest prompt.
const newBackendEventSource = new EventSource(
url,
{withCredentials: false})
console.log('SSE created!');
newBackendEventSource.addEventListener('open', () => {
console.log('SSE opened!');
});
newBackendEventSource.addEventListener('error', (e) => {
console.log('SSE error!');
if (newBackendEventSource.readyState === EventSource.CLOSED) {
// Connection was closed.
console.log('SSE readyState is CLOSED')
}
console.error('Error: ', e);
});
newBackendEventSource.addEventListener('close', (e) => {
console.log('SSE closed!');
const data = JSON.parse(e.data);
console.log("close data: ", data);
newBackendEventSource.close();
});
newBackendEventSource.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
console.log("message data: ", data);
// Use React Updater function to prevent race condition.
// See https://stackoverflow.com/a/26254086/4570472
setPromptsResultsArray((prevPromptsResultsArray) => {
// Since we preprend new results, we need to compute the right index from
// the counter with the equation: length - counter - 1.
// e.g., For counter 2 of a length 3 array, we want index 0.
// e.g., For counter 2 of a length 4 array, we want index 1.
// e.g., For counter 3 of a length 7 array, we want index 4.
// Recall, the counter uses 0-based indexing.
const index = prevPromptsResultsArray.length - data.counter - 1
prevPromptsResultsArray[index] = {
isLoading: false,
prompt: prevPromptsResultsArray[index].prompt,
counter: prevPromptsResultsArray[index].counter,
URIs: [data.uri]}
return prevPromptsResultsArray
});
});
// Add new backend event source to state for persistence.
setBackendEventSources(backendEventSources => [
newBackendEventSource,
...backendEventSources])
}, [prompt]);
This is where my promptsResultsArray is used in the DOM:
{promptsResultsArray.map((promptResults) => {
const promptResultsKey = [promptResults.prompt, promptResults.counter].join("_");
return (
// Add a fragment ( a fake div ) so we can return 2 elements.
<Fragment key={promptResultsKey}>
<p key={`${promptResultsKey}_p1`}>Prompt: {promptResults.prompt}</p>
{/* Creating keys from multiple values: https://stackoverflow.com/a/40425845/4570472*/}
<ImageList cols={1} key={promptResultsKey}>
{promptResults.URIs.map((URI) => (
// Refactor to give each URI its own unique integer ID.
<ImageListItem key={[promptResults.prompt, promptResults.counter, 0].join("_")}>
<img
src={URI}
alt={promptResults.prompt}
style={{height: 260, width: 1034}}
/>
</ImageListItem>
))}
</ImageList>
</Fragment>
)
})}
promptsResultsArray is only updated when prompt is updated. Why? How do I ensure promptsResultsArray is also updated when changed by the SSE?
prevPromptsResultsArraywhich is the same array as the current state so react sees no update due to referential equality. You should clone the array before updating by index, and return the clone. (other problems are that you are creating a newEventSourceon every call of the useEffect without cleaning up any prior instances.)EventSourceon close once I can figure out this current problem