Ichilo
Ichilo

Reputation: 69

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>
    );
}

Upvotes: 0

Views: 407

Answers (2)

Hrishikesh Baidya
Hrishikesh Baidya

Reputation: 567

try this

useEffect(() => {
if (!socketInstance) return; // if no socket instance is created return 
 from here
// if user has connections then update connection list with received 
 connections status update
socketInstance.on("getOnlineUsers", (data) => {
  setConnections((oldConnections) => {
    return oldConnections.length
      ? oldConnections
          .filter((c) => c._id === data.userId)
          .map((c) => {
            c.isOnline = "1";
            return c;
          })
      : oldConnections;
  });
});

socketInstance.on("getOfflineUsers", (data) => {
  setConnections((oldConnections) => {
    return oldConnections.length
      ? oldConnections
          .filter((c) => c._id === data.userId)
          .map((c) => {
            c.isOnline = "0";
            return c;
          })
      : oldConnections;
  });
});

// use socket to listen multiple events

// TODO listen to broadcast message from server & only update messages 
   array if upcoming message is for current user
socketInstance.on("message", (data) => {
  console.log("socket message event", data);
});

return () => {
  // remove socket event listeners once component unmounted
  socketInstance.off("connect");
};
}, [socketInstance]);

Upvotes: 0

BENARD Patrick
BENARD Patrick

Reputation: 30975

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);
    })
}

Upvotes: 2

Related Questions