React Router v6.10 – changing route loses state of previous page

Issue

So I’m fairly new to React and I’m now learning about routing. I created a simple app where I have two pages (Home and Todos) and I route between them through a Tab Bar. On one page I have a list where I can add data to it through events. Code is:

App.tsx

const router = useNavigate();
return (
    <div className='app-main-cont'>
      <div className='app-main-content'>
        <Routes>
          <Route path='/' element={<Navigate replace to='/home'/>}></Route>
          <Route path='/home' element={<Home/>}></Route>
          <Route path='/todos' element={<Todos/>}></Route>
        </Routes>
      </div>

      <nav className='tab-bar'>
        <div className='tab' onClick={()=>{router('/home')}}>HOME</div>
        <div className='tab'  onClick={()=>{router('/todos')}}>TODOS</div>
      </nav>


    </div>

  )

Todos.tsx

function Todos() {

  const [todosList, addNewTodo] = useState<TodoClass[]>([]);
  const [inputValue, changeInputValue] = useState('');

  const handleChangeEvent = (event: ChangeEvent) => {
    const val = (event.target) as HTMLInputElement;
    changeInputValue(val.value);
  }

  const handleAddTaskButtonClick = (event: any) => {
    let obj: TodoClass = {name: inputValue};
    addNewTodo([...todosList, obj]);
  }
    
  return (
    <div className='todos-cont'>
      <div className='list'>
        {todosList.map((item, index) => {
            return (
              <div className='list-item' key={index}>
                {item.name}
              </div>
            )
          })}
      </div>

      <div className='add-todo'>
          <div className='input-todo'>
            <input type="text" placeholder='Input a todo task' onChange={handleChangeEvent} />
          </div>
          <div className='add-btn' onClick={handleAddTaskButtonClick}>
            ADD
          </div>
        </div>
    </div>
  )
}

Now the problem is that when I add data to the list, go to Home and then get back to the Todos page through the navbar, the data that I added before is lost. Why is that? Am I missing something? Thanks!
(The same thing using Ionic-React is not happening that’s why I suppose that I am missing something)

Solution

The todos state is only local state in the Todos component, and when you navigate from "/todos" to "/home" the todos route is no longer matched so the Todos component unmounts. React state is not persisted.

You could lift the todos state up to a common ancestor that remains mounted regardless of what route is matched and pass the state down as props.

Example:

const navigate = useNavigate();

const [todosList, addNewTodo] = useState<TodoClass[]>([]);

const addTodo = (todo: TodoClass) => {
  addNewTodo(todos => [...todos, todo]);
};

return (
  <div className='app-main-cont'>
    <div className='app-main-content'>
      <Routes>
        <Route path="/" element={<Navigate replace to="/home" />} />
        <Route path="/home" element={<Home />} />
        <Route
          path="/todos"
          element={(
            <Todos
              todosList={todosList}
              addTodo={addTodo}
            />
          )}
        />
      </Routes>
    </div>

    <nav className='tab-bar'>
      <div className='tab' onClick={() => router("/home")}>HOME</div>
      <div className='tab'  onClick={() => router("/todos")}>TODOS</div>
    </nav>
  </div>
);
interface TodosProps {
  todosList: TodoClass[];
  addTodo: TodoClass => void;
}

function Todos({ addTodo, todoList }: TodosProps) {
  const [inputValue, changeInputValue] = useState('');

  const handleChangeEvent = (event: ChangeEvent) => {
    const val = (event.target) as HTMLInputElement;
    changeInputValue(val.value);
  }

  const handleAddTaskButtonClick = (event: any) => {
    addTodo({ name: inputValue });
  }
    
  return (
    <div className='todos-cont'>
      <div className='list'>
        {todosList.map((item, index) => (
          <div className='list-item' key={index}>
            {item.name}
          </div>
        ))}
      </div>

      <div className='add-todo'>
        <div className='input-todo'>
          <input type="text" placeholder='Input a todo task' onChange={handleChangeEvent} />
        </div>
        <div className='add-btn' onClick={handleAddTaskButtonClick}>
          ADD
        </div>
      </div>
    </div>
  );
};

An alternative might also be to persist the local todosList state to localStorage, and initialize the state from localStorage. When the component is mounted the todosList state is set to the persisted value or the initial state value []. Use a useEffect hook to persist state to localStorage when it updates.

Example:

function Todos() {
  const [todosList, addNewTodo] = useState<TodoClass[]>(() => {
    // Return persisted state value or default initial value
    return JSON.parse(localStorage.getItem("todosList")) ?? [];
  });

  useEffect(() => {
    if (todosList) {
      // Persist todosList state to localStorage
      localStorage.setItem("todosList", JSON.stringify(todosList));
    }
  }, [todosList]);

  ...
    
  return (
    ...
  );
}

Answered By – Drew Reese

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published