React UseEffect setState doesn't re-render

Viewed 891

I'm experimenting SSE on React, everything is doing well but component is not re-rendered

SOLUTION ON BOTTOM

Here is the code

import React, { useEffect, useState } from "react";
import axios from "axios";

function App() {
  const [streamMessages, setStreamMessages] = useState([]);
  const [eventSource, setEventSource] = useState(null);

  const fetchData = async () => {
    const msgs = await axios.get("http://localhost:3008/message/all/abc123");
    setStreamMessages(msgs.data);
  };

  useEffect(() => {
    fetchData();
    setEventSource(new EventSource("http://localhost:3008/message/abc123"));

    return () => {
      if (eventSource) {
        eventSource.close();
        console.log("eventsource closed");
      }
    };
  }, []);

  useEffect(() => {
    if (eventSource) {
      eventSource.onopen = (event) => {
        console.log("connection opened");
      };

      eventSource.onmessage = (event) => {
        console.log("result", event.data);
        setStreamMessages((old) => [...old, event.data]);
      };

      eventSource.onerror = (event) => {
        console.log(event.target.readyState);
        if (event.target.readyState === EventSource.CLOSED) {
          console.log("eventsource closed (" + event.target.readyState + ")");
        }
        eventSource.close();
      };
    }
  }, [eventSource, streamMessages]);

  return (
    <div className="App">
      <input type="text" />
      <button>Envoyer</button>
      <div>
        {streamMessages.map((message, index) => (
          <p key={index}>
            {message.message} <em>{message.createdAt}</em>
          </p>
        ))}
      </div>
    </div>
  );
}

export default App;

First useEffect fetch 'initial data' and put them in the state

const fetchData = async () => {
    const msgs = await axios.get("http://localhost:3008/message/all/abc123");
    setStreamMessages(msgs.data);
  };

This work as expected, I can see the list of messages

The second useEffect should catch incoming message and add them to initial array of message

useEffect(() => {
    if (eventSource) {
      eventSource.onopen = (event) => {
        console.log("connection opened");
      };

      eventSource.onmessage = (event) => {
        console.log("result", event.data);
        setStreamMessages((old) => [...old, event.data]);
      };

      eventSource.onerror = (event) => {
        console.log(event.target.readyState);
        if (event.target.readyState === EventSource.CLOSED) {
          console.log("eventsource closed (" + event.target.readyState + ")");
        }
        eventSource.close();
      };
    }
  }, [eventSource, streamMessages]);

I can see the "console.log" which means the events are well caught, but my list if message 'streamMessages' is not re-rendered. I put this state 'streamMessages' as dependency of useEffect.

I can't see what's wrong with this code

SOLUTION HERE

Ok I finally found the solution I applied some fix of Dennis Vash (thanks to you). But the main problem was I forgot to JSON parse the data sent by the server

Here is the complete, working, code

import React, { useEffect, useState } from "react";
import axios from "axios";

function App() {
  const [streamMessages, setStreamMessages] = useState([]);
  const [eventSource, setEventSource] = useState(
    () => new EventSource("http://localhost:3008/message/abc123")
  );

  useEffect(() => {
    const fetchData = async () => {
      const msgs = await axios.get("http://localhost:3008/message/all/abc123");
      setStreamMessages(msgs.data);
    };

    fetchData();

    return () => {
      if (eventSource) {
        eventSource.close();
        console.log("eventsource closed");
      }
    };
  }, []);

  useEffect(() => {
    if (eventSource) {
      eventSource.onopen = (event) => {
        console.log("connection opened");
      };

      eventSource.onmessage = (event) => {
        setStreamMessages((old) => [...old, JSON.parse(event.data)]);
      };

      eventSource.onerror = (event) => {
        console.log(event.target.readyState);
        if (event.target.readyState === EventSource.CLOSED) {
          console.log("eventsource closed (" + event.target.readyState + ")");
        }
        eventSource.close();
      };
    }
  }, [eventSource, streamMessages]);

  return (
    <div className="App">
      <input type="text" />
      <button>Envoyer</button>
      <div>
        {streamMessages.map((message, index) => (
          <p key={index}>
            {message.message} <em>{message.createdAt}</em>
          </p>
        ))}
      </div>
    </div>
  );
}

export default App;
1 Answers

Can't assure it works since there might be errors on server (no reproducible example).

So I fixed few mistakes, there are two snippets, working one, and with mistakes commented.

Fixed:

function App() {
  const [streamMessages, setStreamMessages] = useState([]);
  const [eventSource, setEventSource] = useState(
    () => new EventSource("http://localhost:3008/message/abc123")
  );

  // Fetch data init
  useEffect(() => {
    const fetchData = async () => {
      const msgs = await axios.get("http://localhost:3008/message/all/abc123");
      setStreamMessages(msgs.data);
    };

    fetchData();
  }, []);

  // Eventsource setup
  useEffect(() => {
    if (eventSource) {
      eventSource.onopen = (event) => {
        console.log("connection opened");
      };

      eventSource.onmessage = (event) => {
        console.log("result", event.data);
        setStreamMessages((old) => [...old, event.data]);
      };

      eventSource.onerror = (event) => {
        console.log(event.target.readyState);
        if (event.target.readyState === EventSource.CLOSED) {
          console.log("eventsource closed (" + event.target.readyState + ")");
        }
        eventSource.close();
      };
    }
  }, [eventSource]);

  return <>...</>;
}

Mistakes in comments (at least I think those are mistakes):

function App() {
  const [streamMessages, setStreamMessages] = useState([]);
  const [eventSource, setEventSource] = useState(null);

  // should be in scope of useEffect callback
  // not a mistake in this case, but usually has a closure on some stale data
  const fetchData = async () => {
    const msgs = await axios.get("http://localhost:3008/message/all/abc123");
    setStreamMessages(msgs.data);
  };

  useEffect(() => {
    fetchData();

    // unnecessary render, should be as initial value
    setEventSource(new EventSource("http://localhost:3008/message/abc123"));

    return () => {
      // closure on eventSource == null value
      // Should get lint warning here
      if (eventSource) {
        eventSource.close();
        console.log("eventsource closed");
      }
    };
  }, []);

  useEffect(() => {
    // always truthy on streamMessages change
    // therefore will reinit on every streamMessages render

    // should run only once
    if (eventSource) {
      eventSource.onopen = (event) => {
        console.log("connection opened");
      };

      eventSource.onmessage = (event) => {
        console.log("result", event.data);
        setStreamMessages((old) => [...old, event.data]);
      };

      eventSource.onerror = (event) => {
        console.log(event.target.readyState);
        if (event.target.readyState === EventSource.CLOSED) {
          console.log("eventsource closed (" + event.target.readyState + ")");
        }
        eventSource.close();
      };
    }
    // mistake, no need streamMessages in dep array
    // should get lint warning here
  }, [eventSource, streamMessages]);

  return <>...</>;
}
Related