Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

state inside useEffect refers the initial state always with React Hooks

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>
    );
  }
}
like image 727
mikes Avatar asked Feb 13 '19 16:02

mikes


People also ask

Can I set state inside a useEffect hook?

It's ok to use setState in useEffect you just need to have attention as described already to not create a loop. The reason why this happen in this example it's because both useEffects run in the same react cycle when you change both prop.

What does useEffect () hook do in React?

What does useEffect do? By using this Hook, you tell React that your component needs to do something after render. React will remember the function you passed (we'll refer to it as our “effect”), and call it later after performing the DOM updates.

What is useEffect and state in React?

The useEffect Hook allows you to perform side effects in your components. Some examples of side effects are: fetching data, directly updating the DOM, and timers. useEffect accepts two arguments. The second argument is optional. useEffect(<function>, <dependency>)

What is state in React hooks?

The React useState Hook allows us to track state in a function component. State generally refers to data or properties that need to be tracking in an application.


2 Answers

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 };
}
like image 158
Shubham Khatri Avatar answered Oct 04 '22 05:10

Shubham Khatri


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 }; }
like image 28
Pankaj Negi Avatar answered Oct 04 '22 04:10

Pankaj Negi