Search code examples
javascriptreactjsreact-context

React context returns promise


I have a todo app. Im trying to use context api(first time). I have add, delete and get functions in context. I can use add and delete but cant return the get response to state. It returns promise if i log; context. Im using async await. I tried almost everything i know but cant solve it. Where is my fault ? Thank you.

task-context.js

import React, { useReducer } from "react";
import TaskContext from "./task-actions";
import { TaskReducer, ADD_TASK, GET_TASKS, REMOVE_TASK } from "./reducers";

const GlobalState = (props) => {
  const [tasks, dispatch] = useReducer(TaskReducer, { tasks: [] });

  const addTask = (task) => {
      dispatch({ type: ADD_TASK, data: task });
  };

  const removeTask = (taskId) => {
      dispatch({ type: REMOVE_TASK, data: taskId });
  };

  const getTasks = () => {
      dispatch({ type: GET_TASKS });
  };

  return (
    <TaskContext.Provider
      value={{
        tasks: tasks,
        getTasks: getTasks,
        addTask: addTask,
        removeTask: removeTask,
      }}
    >
      {props.children}
    </TaskContext.Provider>
  );
};

export default GlobalState;

reducers.js

import taskService from "../Services/tasks-service";

export const ADD_TASK = "ADD_TASK";
export const GET_TASKS = "GET_TASKS";
export const REMOVE_TASK = "REMOVE_TASK";

const addTask = async (data, state) => {
  console.log("Adding : " + data.title);
  try {
    let task = {
      title: data.title,
      description: data.description,
      comment: data.comment,
      progress: data.status
    };
    const res = await taskService.addNewTask(task);
    console.log(res);
    if (res) {
      getTasks();
    }
  } catch (err) {
    console.log(err);
  }
  return;
};

const getTasks = async () => {
  let response = {}
  try {
    const res = await taskService.loadTasks();
    response = res.data
  } catch (err) {
    console.log(err);
  }
  return { tasks: response }
};

const removeTask = async (data) => {
  try {
    await taskService.deleteTask(data.id);
  } catch (err) {
    console.log(err);
  }
};

export const TaskReducer = (state, action) => {
  switch (action.type) {
    case ADD_TASK:
      return addTask(action.data);
    case GET_TASKS:
      console.log(getTasks());
      return getTasks();
    case REMOVE_TASK:
      return removeTask(action.data);
    default:
      return state;
  }
};

task-actions.js

import React from "react";

export default React.createContext({
  addTask: (data) => {},
  removeTask: (data) => {},
  getTasks: () => {}
});

Solution

  • To start with, you are getting promises returned because you are explicitly returning promises: return addTask(action.data). All your actions are returning promises into the reducer.

    A reducer should be a pure function, meaning that it does not have any side effects (call code outside its own scope), or contain any async functionality, and it should return the same data given the same inputs every single time. You've essentially got the workflow back to front.

    There's a lot to unpick here so I'm going to provide pseudocode rather than try and refactor the entire service, which you will have a more complete understanding of. Starting with the reducer:

    export const TaskReducer = (state, action) => {
      switch (action.type) {
        case ADD_TASK:
          return [...state, action.data];
        case GET_TASKS:
          return action.data;
        case REMOVE_TASK:
          return state.filter(task => task.id !== action.data.id);
        default:
          return state;
      }
    };
    

    This reducer describes how the state is updated after each action is complete. All it should know how to do is update the state object/array it is in charge of. When it comes to fetching data, calling the reducer should be the very last thing you have to do.

    Now on to the actions. The add action is a problem because its not actually returning any data. On top of that, it calls getTasks when really all it ought to do is return one added task (which should be getting returned from await taskService.addNewTask). I would expect that res.data is actually a task object, in which case:

    export const addTask = async (data) => {
      try {
        const task = {
          title: data.title,
          description: data.description,
          comment: data.comment,
          progress: data.status
        };
        const res = await taskService.addNewTask(task);
        return res.data;
      } catch (err) {
        return err;
      }
    };
    

    Similarly for getTasks, I'm going to assume that await taskService.loadTasks returns an array of task objects. In which case, we can simplify this somewhat:

    export const getTasks = async () => {
      try {
        const res = await taskService.loadTasks();
        return res.data;
      } catch (err) {
        return err;
      }
    };
    

    Your removeTask action is essentially fine, although you will want to return errors instead of just logging them.

    Notice we're now exporting these actions. That is so we can now call them from within GlobalState. We're running into issues with name collision so I've just underscored the imported actions for demo purposes. In reality, it might be better to move all the functionality we did in the last step into your taskService, and import that straight into GlobalState instead. Since that's implementation specific I'll leave it up to you.

    import { 
      TaskReducer, 
      ADD_TASK, 
      GET_TASKS, 
      REMOVE_TASK, 
      addTask as _addTask, 
      getTasks as _getTasks, 
      removeTask as _removeTask,
    } from "./reducers";
    
    const GlobalState = (props) => {
      const [tasks, dispatch] = useReducer(TaskReducer, { tasks: [] });
    
      const addTask = async (task) => {
          const added = await _addTask();
    
          if (added instanceof Error) {
            // handle error within the application
           return;
          };
    
          dispatch({ type: ADD_TASK, data: added });
      };
    
      const removeTask = async (taskId) => {
          const removed = await _removeTask(taskId);
    
          if (removed instanceof Error) {
            // handle error within the application
            return;
          };
    
          dispatch({ type: REMOVE_TASK, data: taskId });
      };
    
      const getTasks = async () => {
          const tracks = await _getTracks();
    
          if (tracks instanceof Error) {
            // handle error within the application
            return;
          };
    
          dispatch({ type: GET_TASKS, data: tracks });
      };
    
      ...
    }
    

    Hopefully now you can see how the workflow is supposed to progress. First we call for data from our backend or other API, then we handle the response within the application (for instance, dispatching other actions to notify about errors or side effects of the new data) and then finally dispatch the new data into our state.

    As stated at the beginning, what I've provided is essentially pseudocode, so don't expect it to work out of the box.