Состояние внутри useEffect всегда ссылается на начальное состояние с помощью React Hooks
Каждый раз, когда я отправляю сообщение из другого компонента, я не могу получить полный список сообщений. Вот компонент ловушки и вида:
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 };
}
К сожалению, состояние не сохраняется и показывает всегда последнее сообщение:
export const HookSockets = () => {
const { messages } = useChat();
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message}</div>
))}
</div>
);
};
Если я делаю это обычным способом, все работает как задумано:
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>
);
}
}
2 ответа
Поскольку вы написали свой useEffect для выполнения при первоначальном монтировании компонента, он создает замыкание, которое ссылается на начальное значение messages
и даже если сообщения обновляются, они все равно будут ссылаться на то же значение при последующих вызовах
Вы должны вместо этого настроить useEffect
запускать при первоначальном монтировании и изменении сообщений
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 };
}
или же вы можете использовать шаблон обратного вызова для обновления состояния
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 };
}
Поскольку вы пишете обработчик сокета внутри
useEffect()
с пустым массивом, поэтому этот эффект будет работать только один раз, когда ваш компонент будет монтироваться в первый раз. Функция (или замыкание) запомнит начальное значение сообщений, и даже если сообщения изменят
socket.on()
замыкание по-прежнему будет ссылаться на его начальное значение. Решением этой проблемы будет регистрация наших сообщений в массиве зависимостей эффекта.
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 };
}
Здесь вы столкнетесь с новой проблемой, заключающейся в том, что каждый раз, когда сообщения изменяются, создается новый сокет с обработчиком «сообщения чата», что может привести к неожиданному и многократному запуску дополнительного кода. Чтобы решить эту проблему, вам придется отменить регистрацию предыдущего обработчика. И я рекомендую вам создать сокет только один раз (например, внутри App.js) и передать его как реквизит.
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 }; }