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?

6
  • 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.) Commented Jun 6, 2022 at 18:06
  • 1
    Does this answer your question? How to update an array by index using the useState hook? Commented Jun 6, 2022 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 Commented Jun 6, 2022 at 18:10
  • I'll add logic to clean up the EventSource on close once I can figure out this current problem Commented Jun 6, 2022 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. Commented Jun 6, 2022 at 18:44

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.