Javascript callback doesn’t reference the correct state value

This app shows 3 list of tasks – todo list, in progress list, and done list. I want to increase the time of in-progress tasks every second, but the following code snippet doesn’t work.

./src/contexts/TaskProvider.tsx

/////////////////////////////////////////////////////////////////////////

import React, { createContext, useState, useContext } from 'react';

interface ITask {
  id: number;
  name: string;
  status: 'todo' | 'in-progress' | 'done';
  hourly?: number;
  time?: number;
  price?: number;
  timer?: number;
}

interface ITaskContext {
  tasks?: ITask[];
  createTask?: (name: string, hourly: number) => void;
  startTask?: (id: number) => void;
  resolveTask?: (id: number) => void;
}

const TaskContext = createContext<ITaskContext>({});

const TaskProvider = props => {
  const [tasks, setTasks] = useState<ITask[]>([]);
  const [maxId, setMaxId] = useState<number>(1);

  const createTask: (name: string, hourly: number) => void = (name, hourly) => {
    setTasks([...tasks, { id: maxId, name, status: 'todo', hourly }]);
    setMaxId(maxId + 1);
  };

  const startTask: (id: number) => void = id => {
    // const timer = 0;
    // <!!! bug here !!!>
    const timer = setInterval(() => {
      // here, tasks is different from the array at interval.
      console.log(tasks);

      const newTasks = tasks.map(item => {
        return item.id === id && item.status === 'in-progress'
          ? {
              id,
              name: item.name,
              status: 'in-progress',
              time: item.time !== undefined ? item.time + 1 : 0,
              timer: item.timer,
            }
          : item;
      });
      setTasks(newTasks as ITask[]);
    }, 1000);
    // <!!! bug code ends !!!>

    const newTasks = tasks.map(item =>
      item.id === id
        ? {
            id,
            name: item.name,
            status: 'in-progress',
            hourly: item.hourly,
            time: 0,
            timer,
          }
        : item,
    );
    setTasks(newTasks as ITask[]);
  };

  const resolveTask: (id: number) => void = id => {
    const newTasks = tasks.map(item =>
      item.id === id
        ? {
            id,
            name: item.name,
            status: 'done',
            price:
              (item.hourly ? item.hourly : 0) * (item.time ? item.time : 0),
          }
        : item,
    );
    setTasks(newTasks as ITask[]);
  };

  return (
    <TaskContext.Provider
      value={{ tasks, createTask, startTask, resolveTask }}
      {...props}
    />
  );
};

const useTasks: () => ITaskContext = () => {
  if (TaskContext !== undefined) {
    return useContext<ITaskContext>(TaskContext);
  }

  throw new Error('TaskContext must be used within a TaskProvider');
};

export { TaskProvider, useTasks };
/////////////////////////////////////////////////////////////////////////

As you can see in the comments, tasks id different from the array at each interval. Without this bug snippet, it works correctly without increasing time.

You can get this project from https://github.com/Quanshihe/react-todo-list.git

Answer

The solution is to use useRef() hook as the following.

  ...
  const stateRef = useRef<ITask[]>();
  stateRef.current = tasks;

  const startTask: (id: number) => void = id => {
    const timer = setInterval(() => {
      const newTasks = stateRef.current?.map(item => {
        return item.id === id && item.status === 'in-progress'
          ? {
              id,
              name: item.name,
              status: 'in-progress',
              time: item.time !== undefined ? item.time + 1 : 0,
              timer: item.timer,
            }
          : item;
      });
      setTasks(newTasks as ITask[]);
    }, 1000);

    const newTasks = tasks.map(item =>
      item.id === id
        ? {
            id,
            name: item.name,
            status: 'in-progress',
            hourly: item.hourly,
            time: 0,
            timer,
          }
        : item,
    );
    setTasks(newTasks as ITask[]);
  };
  ...

As a result, it works!!!