0

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?

Rylan Schaeffer
  • 1,945
  • 2
  • 28
  • 50
  • 1
    There are a number of problems with your useEffect, but as regards setting state you are mutating and returning `prevPromptsResultsArray` which 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 new `EventSource` on every call of the useEffect without cleaning up any prior instances.) – pilchard Jun 06 '22 at 18:06
  • 1
    Does this answer your question? [How to update an array by index using the useState hook?](https://stackoverflow.com/questions/65393641/how-to-update-an-array-by-index-using-the-usestate-hook) – pilchard Jun 06 '22 at 18:10
  • @pilchard I'm a complete novice, so any and all feedback is welcome :) Feel free to point out other problems so that I can learn – Rylan Schaeffer Jun 06 '22 at 18:10
  • I'll add logic to clean up the `EventSource` on close once I can figure out this current problem – Rylan Schaeffer Jun 06 '22 at 18:12
  • @pilchard , the link you linked is helpful, but I think this question is uniquely different because I didn't know (and indeed, could not connect) the lack of rendering update to incorrectly updating the state array. – Rylan Schaeffer Jun 06 '22 at 18:44
  • If you add your first comment as an answer, I'll accept it :) – Rylan Schaeffer Jun 06 '22 at 18:44

0 Answers0