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?