Todo-List with useReducer and MongoDB - Error: „todo.map is not a function“

154 views Asked by At

I tried setting up a simple Todolist using nodejs and a MongoDB.

I also used useReducer to simplify all the actions and get a better understanding of it.

I dont know where I went wrong but I get the error ‚todos.map is not a function‘. I guess it has something todo with the fetching of the database-list but I‘m not sure :/

App.js :

import React, { useState, useEffect, useReducer } from "react";
import APIHelper from "./APIHelper.js";
import Todo from './components/Todo'
import "./index.css";

export const ACTIONS = {
  ADD_TODO: 'add-todo',
  TOGGLE_TODO: 'toggle-todo',
  DELETE_TODO: 'delete-todo',
  SET_TODO: 'set-todos'
}

const reducer = (todos, action) => {
  switch (action.type) {
    case ACTIONS.SET_TODOS: {
      return Object.assign({}, todos, {
        todos: action.payload.todos,
      });
    }
    case ACTIONS.ADD_TODO:
      return ([...todos, newTodo(action.payload.name)])
    case ACTIONS.TOGGLE_TODO:
      return todos.map(todo => {
        if(todo.id === action.payload.id) {
          return { ...todo, complete: !todo.complete}
        }
        return todo;
      })
    case ACTIONS.DELETE_TODO:
      return todos.filter(todo => todo.id !== action.payload.id)
    default: 
      return todos;
    }


}

const newTodo = (name) => {
  return { id: Date.now(), name: name, complete: false }
}


export const setTodos = (todos) => {
  return {
    type: ACTIONS.SET_TODOS,
    payload: {
      todos
    },
  };
};



const App = () => {

  const initialState = {
    todos: []
  };

  
  const [todos, dispatch] = useReducer(reducer, initialState);
  const [name, setName] = useState('');

  useEffect(async () => {
    const fetchTodoAndSetTodo = async () => {
      const todos = await APIHelper.getAllTodos();
      return todos;
    };
    const todos = await fetchTodoAndSetTodo();
    console.log(todos);
    dispatch(setTodos(todos));
  }, []);

  const handleSubmit = (e) => {
    e.preventDefault();
    dispatch({ type: ACTIONS.ADD_TODO, payload: {name: name} })
    setName('')
  }

  return (
    
    <div>
      {console.log(todos)};
      <form onSubmit = {handleSubmit}>
        <input type="text" value={name} onChange = {e => setName(e.target.value)}/>
      
      </form>
      {todos.map(todo => {
        return <Todo key={todo.id} todo={todo} dispatch = {dispatch} />
      })}
    </div>
  )
}

export default App;

APIHelper.js:


import axios from "axios";

const API_URL = "http://localhost:8080/todos/";

const createTodo = async (task) => {
  const { data: newTodo } = await axios.post(API_URL, {
    task,
  });
  return newTodo;
};

const deleteTodo = async (id) => {
  const message = await axios.delete(`${API_URL}${id}`);
  return message;
};

const updateTodo = async (id, payload) => {
  const { data: newTodo } = await axios.put(`${API_URL}${id}`, payload);
  return newTodo;
};

const getAllTodos = async () => {
  const { data: todos } = await axios.get(API_URL);
  return todos;
};

export default { createTodo, deleteTodo, updateTodo, getAllTodos };

Todo.js:


import React from "react";
import {ACTIONS} from '../App'

const Todo = ({ todo, dispatch}) => {
  return (
    <div>
        <span style = {{ color: todo.complete ? '#AAA' : '#000'}}>
          {todo.name}
          {console.log(todo.name)}
        </span>
        <button onClick={() => dispatch({ type: ACTIONS.TOGGLE_TODO, payload: {id: todo.id}})}>
          Toggle</button>
        <button onClick={() => dispatch({ type: ACTIONS.DELETE_TODO, payload: {id: todo.id}})}>
          Delete</button>
    </div>
  )}

export default Todo;

I hope someone can help me:(

2

There are 2 answers

0
JMadelaine On

In your reducer you are trying to map the state object, but you should be mapping the todos array of the state object:

case ACTIONS.TOGGLE_TODO:
             // todos is actually the state object
      return todos.map(todo => {

You should either:

1. Rename todos to state

const reducer = (state, action) => {
  switch (action.type) {
    case ACTIONS.SET_TODOS: {
      return {
        ...state,
        todos: action.payload.todos,
      };
    }
    case ACTIONS.ADD_TODO:
      return {
        ...state,
        todos: [...state.todos, newTodo(action.payload.name)],
      };
    case ACTIONS.TOGGLE_TODO:
      return {
        ...state,
        todos: state.todos.map((todo) => {
          if (todo.id === action.payload.id) {
            return { ...todo, complete: !todo.complete };
          }
          return todo;
        }),
      };
    case ACTIONS.DELETE_TODO:
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.payload.id),
      };
    default:
      return state;
  }
};

Or...

2. Change your state object to be an array of todos:

 const initialState = [] // todos array
const reducer = (todos, action) => {
  switch (action.type) {
    case ACTIONS.SET_TODOS: {
      return action.payload.todos;
    }
    case ACTIONS.ADD_TODO:
      return [...todos, newTodo(action.payload.name)];
    case ACTIONS.TOGGLE_TODO:
      return todos.map((todo) => {
        if (todo.id === action.payload.id) {
          return { ...todo, complete: !todo.complete };
        }
        return todo;
      });
    case ACTIONS.DELETE_TODO:
      todos.filter((todo) => todo.id !== action.payload.id);
    default:
      return todos;
  }
};

2
Drew Reese On

Issue

Your initial state is an object

const initialState = {
  todos: []
};

But several of your reducer cases treat state as an array

const reducer = (todos, action) => {
  switch (action.type) {
    case ACTIONS.SET_TODOS: {
      return Object.assign({}, todos, {
        todos: action.payload.todos,
      });
    }
    case ACTIONS.ADD_TODO:
      return ([...todos, newTodo(action.payload.name)]) // <-- here
    case ACTIONS.TOGGLE_TODO:
      return todos.map(todo => { // <-- here
        if(todo.id === action.payload.id) {
          return { ...todo, complete: !todo.complete}
        }
        return todo;
      })
    case ACTIONS.DELETE_TODO:
      return todos.filter(todo => todo.id !== action.payload.id) // <-- here
    default: 
      return todos;
    }
}

Solution

Update reducer logic to access todo state correctly. I suggest calling it "state" and then accessing state.todos for the current state values. Each case needs to return an object with a todos key.

const reducer = (state, action) => {
  switch (action.type) {
    case ACTIONS.SET_TODOS:
      return {
        todos: action.payload.todos,
      });
    
    case ACTIONS.ADD_TODO:
      return {
        todos: [...state.todos, newTodo(action.payload.name)]
      }

    case ACTIONS.TOGGLE_TODO:
      return {
        todos: state.todos.map(todo => {
          if(todo.id === action.payload.id) {
            return { ...todo, complete: !todo.complete}
          }
          return todo;
        })
      }

    case ACTIONS.DELETE_TODO:
      return {
        todos: state.todos.filter(todo => todo.id !== action.payload.id)
      }

    default: 
      return state;
    }
}

You should also update your UI logic to reflect this nesting

const [state, dispatch] = useReducer(reducer, initialState);

...

{state.todos.map(todo => {
  return <Todo key={todo.id} todo={todo} dispatch = {dispatch} />
})}

Alternative Solution

If you don't intend to store anything else in your reducer state then it may be a little easier to make your state just the array.

Update the SET_TODOS action handler to spread (i.e. copy) the todos from the action into a new array reference.

const reducer = (todos, action) => {
  switch (action.type) {
    case ACTIONS.SET_TODOS:
      return [...action.payload.todos]; // <-- spread into array
    
    case ACTIONS.ADD_TODO:
      return [...todos, newTodo(action.payload.name)]

    case ACTIONS.TOGGLE_TODO:
      return todos.map(todo => {
        if(todo.id === action.payload.id) {
          return { ...todo, complete: !todo.complete}
        }
        return todo;
      });
    
    case ACTIONS.DELETE_TODO:
      return todos.filter(todo => todo.id !== action.payload.id)

    default: 
      return todos;
    }
}

Update initialState to be an empty array.

 const initialState = []; // <-- just the array

Now todos is the state and is just an array, so any todos.map will work as it appears you were expecting.

const [todos, dispatch] = useReducer(reducer, initialState);