Context Api state is not changing

Viewed 440

im trying to call a function called deleteTask inside the Context Provider, from a component that consumes the context using the useContext hook, which deletes a certain item from an array in the state of the context provider, but when i do it, the state of the provider doesnt change at all, i try to follow the problem and the function excecutes but it seems like if it was excecuting in the scope of a copied Provider? Also tried a function to add a task and im having the same issue. I also added a function to set the active task, and i dont know why that one did work, while the others dont. I dont really know whats happening, here is the code, pleeeeease help me:

tasks-context.jsx

import React, { useState } from 'react';
import { useEffect } from 'react';

const dummyTasks = [{
  task: {
    text: 'hello',
  },
  key: 0,
  isActive: false
},
{
  task: {
    text: 'hello 2',
  },
  key: 1,
  isActive: false
}];

export const TasksContext = React.createContext({ });

export const TasksProvider = ( props ) => {
  const [ tasks, setTasks ] = useState( dummyTasks );
  const [ activeTask, setActiveTask ] = useState();

    
  //NOT WORKING
  const deleteTask = ( taskToDeleteKey ) =>{
    setActiveTask( null );
    setTasks( tasks.filter( task => task.key !== taskToDeleteKey ));
  };

  //THIS ONE WORKS (??)
  const handleSelectTask = ( taskToSelect, key ) =>{
    setActiveTask( taskToSelect );
    const newTaskArray = tasks.map( task => {
      if( task.key === key ){
        task.isActive = true;
      }else{
        ficha.isActive = false;
      }
      return task;
    });
    setTask( newTaskArray );
  };

  return ( <TasksContext.Provider 
      value={{ tasks,
               activeTask,
               addTask,
               deleteTask,
               handleSelectTask}}>
    {props.children}
  </TasksContext.Provider>
  );
};

the "main"

Main.jsx

import React from 'react';
import './assets/styles/gestion-style.css';
import './assets/styles/icons.css';

import { TasksProvider } from '../../Context/tasks-context';

import TaskContainer from './components/taskContainer.jsx';

function Main( props ) {
  
 return (
      <TasksProvider>
        <TaskContainer />
      </TasksProvider>
    );
}

the task container maps the array of tasks:

TaskContainer.jsx

import React, { useContext, useEffect } from 'react';
import TaskTab from './TaskTab';


import { TasksContext } from '../../Context/tasks-context';

function TaskContainer( props ) {
  const { tasks } = useContext( TasksContext );

  return (
    <div className="boxes" style={{ maxWidth: '100%', overflow: 'hidden' }}>
      {tasks? tasks.map( taskTab=>
        ( <TaskTab task={taskTab.task} isActive={taskTab.isActive} key={taskTab.key} taskTabKey={taskTab.key} /> ))
        :
        null
      }
    </div>
  );
}

export default TaskContainer;

And the task component from which i call the context function to delete:

TaskTab.jsx

import React, { useContext } from 'react';

import { TasksContext } from '../../Context/tasks-context';

function TaskTab( props ) {
  let { task, isActive, taskTabKey } = props;
  const { handleSelectTask, deleteTask } = useContext( TasksContext );

  const selectTask = ()=>{
    handleSelectTask( task, taskTabKey );
  };

  const handleDelete = () =>{
    deleteTask( taskTabKey );
  };

  return (
    <div onClick={ selectTask }>
      <article className={`${task.type} ${isActive ? 'active' : null}`}>
        <p className="user">{task.text}</p>
        <button onClick={handleDelete}>
          <i className="icon-close"></i>
        </button>
      </article>
    </div>
  );
}

export default TaskTab;
2 Answers

Lucas, this is not an issue with Context or Provider.

The problem that you are facing is actually a mechanism known as event bubbling where the current handler executes followed by parent handlers.

More info on event bubbling could be found here. https://javascript.info/bubbling-and-capturing.

In your case first, the handleDelete function gets called followed by handleSelect function.

Solution: event.stopPropagation();

Change your handleDelete and handleSelect function to this

  const selectTask = () => {
    console.log("handle select called");
    handleSelectTask(task, taskTabKey);
  };

  const handleDelete = event => {
    console.log("handle delete called");
    event.stopPropagation();
    deleteTask(taskTabKey);
  };

Now check your console and you will find only the handle delete called will print and this would solve your problem hopefully.

If it still doesn't work then do let me know. I will create a codesandbox version for you.

Happy Coding.

Thanks for the great question!

What is happening here is understandably confusing, and it took me a while to realize it myself.

TL;DR: handleSelectTask in the Provider is being called every time a button is clicked for deleteTask because of event propagation. handleSelectTask isn't using the state that has been modified by deleteTask, even though it's running after it, because it has closure to the initial tasks array.


Quick Solution 1

Stop the event from propagating from the delete button click to the TaskTab div click, which is probably the desired behavior.

// in TaskTab.jsx
const handleDelete = (event) => {
  event.stopPropagation(); // stops event from "bubbling" up the tree
  deleteTask(taskTabKey);
}

In the DOM (and emulated by React as well), events "bubble" up the tree, so that parent nodes can handle events coming from their child nodes. In the example, the <button onClick={handleDelete}> is a child of the <div onClick={selectTask}>, which means that when the click event is fired from the button, it will first call the handleDelete function like we want, but it will also call the selectTask function from the parent div afterwards, which is probably unintended. You can read more about event propagation on MDN.


Quick Solution 2

Write the state updates to use the intermediary state value at the time they are called.

// in tasks-context.jsx
const deleteTask = ( taskToDeleteKey ) => {
  setActiveTask(null);
  // use the function version of setting state to read the current value whenever it is run
  setTasks((stateTasks) => stateTasks.filter(task => task.key !== taskToDeleteKey));
}

const handleSelectTask = ( taskToSelect, key ) =>{
  setActiveTask( taskToSelect );
  // updated to use the callback version of the state update
  setTasks((stateTasks) => stateTasks.map( task => {
    // set the correct one to active
  }));
};

Using the callback version of the setTasks state update, it will actually read the value at the time the update is being applied (including and especially in the middle of an update!), which, since the handleSelectTask is called after, means that it actually sees the array that has already been modified by the deleteTask that ran first! You can read more about this callback variant of setting state in the React docs (hooks) (setState). Note that this "fix" will mean that your component will still call handleSelectTask even though the task has been deleted. It won't have any ill-effects, just be aware.


Let's walk through what's happening in a bit more detail:

First, the tasks variable is created from useState. This same variable is used throughout the component, which is totally fine and normal.

// created here
const [ tasks, setTasks ] = useState( dummyTasks );
const [ activeTask, setActiveTask ] = useState();

const deleteTask = ( taskToDeleteKey ) =>{
  setActiveTask( null );
  // referenced here, no big deal
  setTasks( tasks.filter( task => task.key !== taskToDeleteKey ));
};

const handleSelectTask = ( taskToSelect, key ) =>{
  setActiveTask( taskToSelect );
  // tasks is referenced here, too, awesome
  const newTaskArray = tasks.map( task => {
    if( task.key === key ){
      task.isActive = true;
    }else{
      task.isActive = false;
    }
    return task;
  });
  setTasks( newTaskArray );
};

Where the trouble comes in, is that if both of the functions are trying to update the same state value in the same render cycle, they will both be referencing the original value of the tasks array, even if the other function has attempted to update the state value! In your case, because the handleSelectTask is running after deleteTask, this means that handleSelectTask will update state using the array that hasn't been modified! When it runs, it will still see two items in the array, since the tasks variable won't change until the update is actually committed and everything rerenders. This makes it look like the delete portion isn't functioning, when really its effect is just being discarded since handleSelectTask isn't aware that the delete happened before it.

Related