state inside useEffect refers the initial state always with React Hooks
Asked Answered
A

2

16

Every time I emit a message from another component, I can't get the full list of messages. Here is the hook and view component:

export function useChat() {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const socket = openSocket("http://localhost:3003");
    socket.on("chat message", msg => {
      const newState = update(messages, { $push: [msg] });
      setMessages(newState);
    });
  }, []);

  return { messages };
}

Unfortunately the state doesn't persist and shows always the last message:

export const HookSockets = () => {
  const { messages } = useChat();
  return (
    <div>
      {messages.map((message, index) => (
        <div key={index}>{message}</div>
      ))}
    </div>
  );
};

If I do this the regular way, everything works as intended:

export class ClassSockets extends Component {
  state = {
    socket: openSocket("http://localhost:3003"),
    messages: [],
    message: ""
  };

  componentDidMount() {
    this.state.socket.on("chat message", msg => {
      const newState = update(this.state, {
        messages: { $push: [msg] }
      });
      this.setState(newState);
    });
  }

  handleClick = () => {
    this.state.socket.emit("chat message", this.state.message);
    this.setState({ message: "" });
  };

  handleChange = event => {
    this.setState({ message: event.target.value });
  };

  render() {
    return (
      <div>
        <div>Sockets</div>
        <div>{this.state.messages}</div>
        <input
          type="text"
          value={this.state.message}
          onChange={this.handleChange}
        />
        <button onClick={this.handleClick}>Send Message</button>
      </div>
    );
  }
}
Afreet answered 13/2, 2019 at 16:53 Comment(0)
E
24

Since you have written your useEffect to execute on initial mount of component, it creates a closure which references the initial value of messages and even if the messages update, it will still refer to the same value on subsequent calls

You should instead configure the useEffect to run on initial mount and messages change

export function useChat() {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const socket = openSocket("http://localhost:3003");
    socket.on("chat message", msg => {
      const newState = update(messages, { $push: [msg] });
      setMessages(newState);
    });
  }, [messages]);

  return { messages };
} 

or else you could use the callback pattern to update state

export function useChat() {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const socket = openSocket("http://localhost:3003");
    socket.on("chat message", msg => {
      setMessages(prevMessages => update(prevMessages, { $push: [msg] }););
    });
  }, []);

  return { messages };
}
Evangelist answered 14/2, 2019 at 6:59 Comment(0)
L
1

As you are writing socket handler inside the useEffect() with an empty array, So this effect will run only once when your component will mount for the first time. The socket.on() function (or closure) will memorize the initial value of the messages and even if the messages gets change the socket.on() closure will still refer to its initial value. Solution for this problem will be to register our messages to the dependency array of effect.

export function useChat() {   
const [messages, setMessages] = 
useState([]);

useEffect(() => {
const socket = openSocket("http://localhost:3003");
socket.on("chat message", msg => {
  const newState = update(messages, { $push: [msg] });
  setMessages(newState);
});   }, [messages]);
return { messages }; 
}

Here a new problem you will encounter that each time messages get changed a new socket with "chat message" handler is created which may result unexpected and addition code to run multiple times. To solve that issue you will have to de-register the earlier handler. And I'll recommend you to create socket only once (e.g. inside App.js) and pass it as a props.

export function useChat(socket) {   
const [messages, setMessages] = useState([]);

useEffect(() => { 
socket.on("chat message", msg => {
  const newState = update(messages, { $push: [msg] });
  setMessages(newState);
});   
//De-register old handler   
 return function(){
socket.off("chat message")   } }, [messages]);

return { messages }; }
Lemay answered 5/1, 2022 at 18:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.