Home > Enterprise >  useReducer dispatch being called twice
useReducer dispatch being called twice

Time:05-01

I have a to do's list that has a <li> with an edit Button and Toggle Button.

Actually i've made a reducer to do the actions: add, delete, toggle. It's almost working fine, but it is executing the actions twice. The toggle function for example is putting the attribute done to true, then the function execute again and the toggle function put the attribute to false again.

Here is my project in github: https://github.com/brenosantin96/TodoListWithUseReducer01

Here is the Reducer code i've did:

function reducer(stateTodos: todoType[], actionType: actionType) {

switch (actionType.type) {
    case 'ADD-TODO':
        if (actionType.payload?.id && actionType.payload?.name) {
            let newStateTodos = [...stateTodos];
            newStateTodos.push({
                id: actionType.payload?.id,
                name: actionType.payload?.name,
                done: actionType.payload?.done
            })
            console.log("Adicionado TODO");
            return newStateTodos;
            break;

        }

    case 'DEL-TODO':
        if (actionType.payload?.id) {
            let newStateTodos = [...stateTodos];
            newStateTodos = newStateTodos.filter((item) => item.id !== actionType.payload?.id)
            console.log("Deletado TODO");
            return newStateTodos;
            break;

        }

    case 'TOGGLE-TODO':
        if (actionType.payload?.id) {
            let newStateTodos = [...stateTodos];

            for (let item of newStateTodos) {
                if (item.id === actionType.payload?.id) {
                    item.done = !item.done
                }
            }

            console.log(newStateTodos);
            return newStateTodos;
        }

    default:
        return stateTodos;

}

}

Below is the function component:

function App2() {

const [inputTodo, setInputTodo] = useState('');
const [stateTodos, dispatch] = useReducer(reducer, initialToDos);


//controlling input always when value changes
const handleInputTodo = (e: ChangeEvent<HTMLInputElement>) => {
    setInputTodo(e.target.value);
}

//Function that makes add todo.
const handleForm = (e: React.FormEvent) => {
    e.preventDefault();
    dispatch({
        type: 'ADD-TODO', payload: {
            id: parseInt(uuidv4()),
            name: inputTodo,
            done: false
        }
    });

    setInputTodo('');

}

//Function that calls deleteTodo to delete a todo
const handleDelTodo = (id: number) => {
    dispatch({
        type: 'DEL-TODO', payload: {
            id: id
        }
    });


}

//Funcao that calls Toggle-TODO, it should toggle the done to false or the false to true.
const handleToggleTodo = (id: number) => {
    dispatch({
        type: 'TOGGLE-TODO', payload: {
            id
        }
    })
}


return (
    <div>
        <form action="submit" onSubmit={handleForm}>
            <input type="text" value={inputTodo} onChange={handleInputTodo} />
            <button type='submit'>ENVIAR</button>
        </form>

        <div>
            LIST:
            <ul>
                {stateTodos.map((item) => {
                    return (
                        <li key={item.id}>
                            {item.id} - {item.name}
                            <button onClick={() => handleDelTodo(item.id)}> DELETE </button>
                            <button onClick={() => handleToggleTodo(item.id)}>CHECK</button>
                        </li>
                    )
                })}
            </ul>
        </div>




    </div>
)

}

Any idea of where im missing something ?

CodePudding user response:

It has to be something external to that code. I have copied the file and adapted to javascript (because the project that I have is in JS). It works completely fine (action only be executed once).

/*
Question: useReducer dispatch being called twice
Url: https://stackoverflow.com/questions/72068781/usereducer-dispatch-being-called-twice
 */

import { v4 as uuidv4 } from 'uuid';

import { useReducer, useState } from 'react';

function reducer(stateTodos, actionType) {
  switch (actionType.type) {
    case 'ADD-TODO':
      if (actionType.payload.id && actionType.payload.name) {
        const newStateTodos = [...stateTodos];
        newStateTodos.push({
          id: actionType.payload.id,
          name: actionType.payload.name,
          done: actionType.payload.done,
        });
        console.log('Adicionado TODO');
        return newStateTodos;
        break;
      }

    case 'DEL-TODO':
      if (actionType.payload.id) {
        let newStateTodos = [...stateTodos];
        newStateTodos = newStateTodos.filter((item) => item.id !== actionType.payload.id);
        console.log('Deletado TODO');
        return newStateTodos;
        break;
      }

    case 'TOGGLE-TODO':
      if (actionType.payload.id) {
        const newStateTodos = [...stateTodos];

        for (const item of newStateTodos) {
          if (item.id === actionType.payload.id) {
            item.done = !item.done;
          }
        }

        console.log(newStateTodos);
        return newStateTodos;
      }

    default:
      return stateTodos;
  }
}

const initialToDos = [];

function Question19() {
  const [inputTodo, setInputTodo] = useState('');
  const [stateTodos, dispatch] = useReducer(reducer, initialToDos);

  // controlando o input para sempre que digittar atualizar o valor do proprio.
  const handleInputTodo = (e) => {
    setInputTodo(e.target.value);
  };

  // Funcao que faz acao de adicionar Todo ao dar submit no FORM
  const handleForm = (e) => {
    e.preventDefault();
    dispatch({
      type: 'ADD-TODO',
      payload: {
        id: parseInt(uuidv4()),
        name: inputTodo,
        done: false,
      },
    });

    setInputTodo('');
  };

  // Funcao que faz acao de deletar TODO ao clicar no botao de delete
  const handleDelTodo = (id) => {
    dispatch({
      type: 'DEL-TODO',
      payload: {
        id,
      },
    });
  };

  // Funcao que chama o Toggle-TODO, chama a action Toggle TODO e altera o status DONE para FALSE (nao esta funcionando ainda)
  const handleToggleTodo = (id) => {
    dispatch({
      type: 'TOGGLE-TODO',
      payload: {
        id,
      },
    });
  };

  return (
    <div>
      <form action="submit" onSubmit={handleForm}>
        <input type="text" value={inputTodo} onChange={handleInputTodo} />
        <button type="submit">ENVIAR</button>
      </form>

      <div>
        LISTA:
        <ul>
          {stateTodos.map((item) => (
            <li key={item.id}>
              {item.id} - {item.name}
              <button onClick={() => handleDelTodo(item.id)}> DELETAR </button>
              <button onClick={() => handleToggleTodo(item.id)}>CHECK</button>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

export default Question19;

CodePudding user response:

React invoke reducer several times for internal purpose. This happens when you are using Strict Mode. When you remove it you can clearly see react call only once. This happens in development mode only, if you switch to production mode it wont call twice or more than that. You can have more details on that useReducer dispatch calls reduce twice, it's briefly discussed there.

Another issue is toggleFunction always returns false. This is again due to React invoking it which makes !false -> true and on next call it changin !true -> false. So evenly calls will always return false and odd calls will make it true alway. You can solve it by using pure functions. In redux app you have to make sure everything is pure. Its fundamental to use pure function in your reducer. So with that every time you provide same input you get same output.

case 'TOGGLE-TODO':
  if (actionType.payload?.id) {
    let newStateTodos = [...stateTodos];

    for (let item of newStateTodos) {
      if (item.id === actionType.payload?.id) {
        item.done = !item.done
      }
    }

    console.log(newStateTodos);
    return [...newStateTodos];
  }

Here you are passing just id and expecting it to mutate state on its own value, which is not a good idea. Along with id you also have to pass new state for done property. So no matter how many you calls it with same parameter you will always get same output.

You can change toggle functionality with below snippet

case 'TOGGLE-TODO':
  const {id, done} = actionType.payload;
  if (id) {
    let newStateTodos = [...stateTodos];

    newStateTodos.find(t => t.id === id).done = done;

    return newStateTodos;
  }

and also update dispatch call.

const handleToggleTodo = (id) => {
    dispatch({
        type: 'TOGGLE-TODO', payload: {
            id,
            done: !stateTodos.find(t => t.id === id).done
        }
    })
}

You can improve this if create separate component for each to-do list item. And get rid of finding specific todo done state.

  • Related