Search code examples
javascriptreactjsreact-hookssocket.io

Can't use react state inside a socketio on event callback function


I am trying to make a component where when you add a task, an event listener gets added to the socketio client. The event gets triggered when the server reports the progress of the task and I want to update the task with the new progress.

The problem is that when I update the task progress, the event callback function uses the state of the component when the function got assigned. If the "tasks" state was empty prior to assignment, the progress update erases the state.

I don't know how I can use the current state inside the callback function. (The progress update does work outside of the event callback)

Here is a minimal reproducible example:

import React, {useEffect, useRef, useState} from "react";
import axios from "axios";
import socketIOClient from "socket.io-client"


export default function TestComponent() {
    const socketRef = useRef(null);
    const [tasks, setTasks] = useState([])  // task: {id, name, time_start, progress}

    useEffect(() => {
        if (socketRef.current == null) {
            socketRef.current = socketIOClient({path: "/api/socket.io"});
        }

        return () => {
            socketRef.current.disconnect();
        };
    }, []);

    function addTask(task) {
        setTasks([...tasks, task]);
        addTaskProgressEvent(task);
    }

    function addTaskProgressEvent(task) {
        const event = "task:" + task.id + ":progress";

        socketRef.current.on(event, (progress) => {
            // update task progress (this failes!)
            setTasks(tasks.map(list_task => list_task.id === task.id ?
                {...list_task, progress: progress} : list_task))

            console.log(progress);
        })
    }

    function handleStartTask() {
        axios.post("/api/task").then((response) => {
            addTask(response.data);
        });
    }

    return (
        <div className="TestComponent">
            <button onClick={handleStartTask}>
                start task
            </button>
        </div>
    );
}

Solution

  • You have to use the setter with callback, in order to get the current value of the state :

    function addTaskProgressEvent(task) {
        const event = "task:" + task.id + ":progress";
    
        socketRef.current.on(event, (progress) => {
            // update task progress (this failes!)
            setTasks(oldTasks => oldTasks.map(list_task => list_task.id === task.id ?
                {...list_task, progress: progress} : list_task))
    
            console.log(progress);
        })
    }