skip to Main Content

I am doing a simple todo list to start learning React. I have the following file.

The issue I’m running into is during the form submission callback handleSubmit. The first time I enter a new todo, lets say with the value hello, todos is updated and the view redraws, however, localStorage isn’t updated and the input isnt cleared by the call to setTodoEntry.

On a subsequent form submission, say with the value world, todos is updated and the view redraws, however this time localStorage is updated with the value from the previous call (todoEntry === 'hello'). The input still isnt cleared by setTodoEntry. On subsequent form submissions localStorage is always one "step" behind, and the input never clears.

import { useState } from 'react'
import './App.css'
import Todo from './components/todo'
import { TextField, Button } from '@mui/material';
import { nanoid } from 'nanoid';

function App() {
  let myStoredTodos = localStorage.getItem('todos');
  const [todos, setTodos] = useState(myStoredTodos ? JSON.parse(myStoredTodos) : []); 
  const [todoEntry, setTodoEntry] = useState('');
  
  function handleTodoEntry(event) {
    setTodoEntry(event.target.value);
  }

  function handleSubmit(event) {
    event.preventDefault();
    if (!todoEntry) return;
    setTodos([ { id: nanoid(), checked: false, value: todoEntry }, ...todos ])
    localStorage.setItem('todos', JSON.stringify(todos));
    setTodoEntry('');
  }

  return (
    <>
      <h1>{`//TODO`}</h1>
      <form onSubmit={handleSubmit}>
        <TextField variant="standard" onChange={(event) => handleTodoEntry(event)} />
        <Button id="add-button" type="submit" variant="contained" disabled={!todoEntry.length}>Add</Button>
      </form>
      
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <Todo checked={todo.checked} value={todo.value} />
          </li>
        ))}
      </ul>
    </>
  )
}

export default App;

2

Answers


  1. setTodos will schedule the update but doesn’t immediately apply it. When you call localStorage.setItem('todos', JSON.stringify(todos)); after setTodos, the todos array hasn’t been updated yet.

    Use the previous state in the SetTodos function:

    function handleSubmit(event) {
      event.preventDefault();
      if (!todoEntry) return;
      setTodos((prevTodos) => {
        const newTodos = [{ id: nanoid(), checked: false, value: todoEntry }, ...prevTodos];
        localStorage.setItem('todos', JSON.stringify(newTodos));
        return newTodos;
      });
      setTodoEntry('');
    }
    

    If the input is not cleared, make sure you bind the todoEntry state to the TextField’s value:

    <TextField variant="standard" value={todoEntry} onChange={(event) => handleTodoEntry(event)} />

    Login or Signup to reply.
  2. React state updates are asynchronous and the value of todos is essentially fixed inside the closure. You can instead get the new value after the next render inside an effect.

    useEffect(() => {
        localStorage.setItem('todos', JSON.stringify(todos));
    }, [todos]);
    

    Also, to keep the TextField controlled, set its value to todoEntry. (See the Uncontrolled vs Controlled section of the MUI Text Field documentation.)

    <TextField variant="standard" value={todoEntry} onChange={(event) => handleTodoEntry(event)} />
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search