1

I'm having an issue with useReducer + Typescript + async. I just can't do it! When I call anything from async function it return a Promise which break my code. When I tried to get it other way, the component is doesn't re-render! That is Driving me crazy.

I wrote this issue on my personal project which represents the problem I have! https://github.com/igormcsouza/full-stack-todo/issues/15

What I can do to make it work?

I want to make a call from the backend populate the list with the information I got from backend. So my frontend need to re-render every time any change is done to the backend (when add, update or delete any registry there).

reducers.tsx

import { delete_todo, fetch_todos, insert_todo, update_todo } from 
"../utils";
import { State, Actions, Todo } from "../TodoContext";

export const INITIAL_STATE: State = {
  todos: [],
};

export const reducer = (state: State, action: Actions): State => {
  let newState: State = {};

  switch (action.type) {
    case "POPULATE":
      fetch_todos().then((value) => (newState = value));
      return newState;

    case "ADD_TODO":
      if (state.todos) {
        const newTodo: Todo = {
          when: (+new Date()).toString(),
          task: action.payload,
          checked: false,
          by: "Igor Souza",
        };

        insert_todo(newTodo);
      }

      fetch_todos().then((value) => (newState = value));
      return newState;

    case "CHECK_TODO":
      action.payload.checked = !action.payload.checked;

      update_todo(action.payload);

      fetch_todos().then((value) => (newState = value));
      return newState;

    case "EDIT_TODO":
      let todo = action.payload.task;

      todo.task = action.payload.newTaskName;
      update_todo(todo);

      fetch_todos().then((value) => (newState = value));
      return newState;

    case "DELETE_TODO":
      delete_todo(action.payload);

      fetch_todos().then((value) => (newState = value));
      return newState;

    default:
      return state;
  }
};

utils.tsx (with the axios calls)

import axios from "axios";

import { State, Todo } from "./TodoContext";
// const base = "http://backend:2500";
const base = "https://full-stack-todo-bknd.herokuapp.com";

export async function fetch_todos(): Promise<State> {
  let todos: State = {};

  await axios
    .get<State>(base + "/api/todo")
    .then((response) => {
      const { data } = response;
      todos = data;
    })
    .catch((e) => console.log(e));

  console.log(typeof todos.todos);
  return todos;
}

export async function insert_todo(todo: Todo) {
  await axios.post(base + "/api/todo", todo).catch((e) => console.log(e));
}

export async function update_todo(todo: Todo) {
  await axios.put(base + "/api/todo/" + todo.id).catch((e) => console.log(e));
}

export async function delete_todo(todo: Todo) {
  await axios
    .delete(base + "/api/todo/" + todo.id)
    .catch((e) => console.log(e));
}

context.tsx (Context APi)

import React, { createContext, useReducer } from "react";

import { reducer, INITIAL_STATE } from "./reducers";

type ContextProps = {
  state: State;
  dispatch: (actions: Actions) => void;
};

export interface Todo {
  id?: string;
  task: string;
  when: string;
  checked: boolean;
  by: string;
}

export interface State {
  todos?: Array<Todo>;
}

export interface Actions {
  type: string;
  payload?: any;
}

export const TodoContext = createContext<Partial<ContextProps>>({});

const TodoContextProvider: React.FC = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, INITIAL_STATE);

  return (
    <TodoContext.Provider value={{ state, dispatch }}>
      {children}
    </TodoContext.Provider>
  );
};

export default TodoContextProvider;

1 Answers1

0

Put simply, what you are trying to do is not possible. You cannot have a reducer that is asynchronous. This means that you need to move the async logic outside of the reducer itself.

The reducer is just responsible for applying the data from the action to the state. Since you are re-fetching the whole list after every action (not ideal) you only have one real action which is to replace the whole state. You would do the aysnc fetching and then refresh the state.

export const populate = (dispatch: Dispatch<Actions>) => {
  fetch_todos().then((data) =>
    dispatch({
      type: "POPULATE",
      payload: data
    })
  );
};
export const reducer = (state: State, action: Actions): State => {
  switch (action.type) {
    case "POPULATE":
      return action.payload;
...
<button onClick={() => populate(dispatch)}>Populate</button>

Passing the dispatch function to an action creator is called a "thunk" and it's a popular pattern with Redux. We don't have any middleware, so we just directly call populate(dispatch) instead of something like dispatch(populate()).


Look for ways that you can streamline your code.

We can make use of the fact that all our actions call the same fetch_todos() in order to simplify things (for now -- eventually you want to not refresh the entire list after every change).

insert_todo, update_todo, and delete_todo are all extremely similar. The main difference is the axios method which can be passed as an argument with axios.request.

Though the more I look, the more I see that they should be less similar! You need to pass the todo data on your put request. You want the id property on Todo to be required and for add_todo to take Omit<Todo, 'id'>.


The inverted approach would be to make changes directly to the reducer state first. Then use a useEffect to detect changes and push the to the backend.

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102