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>
);
}
}
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 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.
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>)
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.
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 };
}
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 }; }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With