Search code examples
reactjsreduxreact-reduxlocal-storageredux-persist

Redux-Persist not persisting one slice of the state


I'm using react-redux with redux-persist in a React application with multiple slices of state. It is a simple app with slices such as ui, todolist, notes, and todolist-projects. All of the slices of state are persisting except for the todolist slice.

Below is my store/index.js where I initialize the store and persistor:

import { legacy_createStore as createStore } from "redux";
import { persistStore, persistReducer } from "redux-persist";
import { rootReducer } from "./rootReducer";
import storage from "redux-persist/lib/storage";

const persistConfig = {
  key: "root",
  storage,
};

const persistedReducer = persistReducer(persistConfig, rootReducer);

export const store = createStore(
  persistedReducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
export const persistor = persistStore(store);

I read something inside of a GitHub issue about the key prop sometimes needing to be set to "primary" instead of "root". In the Chrome DevTools, in the localStorage debugging area, I noticed that the todolist slice does seem to be persisting in the persist:primary section. (see the screenshot below.) Screenshot from Chrome DevTools

I tried changing key from "root" to "primary" but this does not seem to be working. It didn't seem to make a difference.

You can see this project live on CodeSandbox


Solution

  • Issues

    The state.todolist is being persisted to localStorage, as evidenced by your screenshot. There were a few issues I found in the code in your sandbox though.

    1. NavBar is using raw anchor (<a>) tags instead of the react-router-dom Link component to effect navigation actions. These will reload the page and reset any local React component state.
    2. The Todolist component was using an incorrect object reference comparison to filter the selected state.todolist state against the current state.ui.currTodoProject value. These are both objects and after the state has been JSON serialized into localStorage and then JSON deserialized back into the Redux store, these will no longer have shallow reference equality... they are completely new and different objects.

    Suggested Solution

    Update NavBar to use the Link component.

    import "./NavBar.styles.css";
    import { Link } from "react-router-dom";
    import { persistor } from "../../../store/index.js";
    
    const NavBar = () => {
      const purgeCache = () => {
        persistor
          .purge()
          .then(() => console.log("Cache Pruged!"))
          .catch(() => console.log("Could Not Purge Cache..."));
      };
      
      return (
        <nav>
          <ul>
            <li>
              <Link to="/todolist">Todo List</Link>
            </li>
            <li>
              <Link to="/notes">Notes</Link>
            </li>
            <li style={{ float: "right" }} onClick={purgeCache}>
              <Link to="/">Purge Store</Link>
            </li>
          </ul>
        </nav>
      );
    };
    
    export default NavBar;
    

    Update Todolist compare the project objects *by their _id properties. There's also no need to duplicate the todolist into any local state, simply compute the current currTodos value from the selected todolist state.

    const currTodos = todolist.filter(
      (t) => t.project._id === ui.currTodoProject._id
    );
    

    If computed this derived state is expensive use the useMemo hook to memoize the computed value. In fact, just about any time you see yourself writing a useState/useEffect hook combo, this is what useMemo is for.

    const currTodos = useMemo(() => todolist.filter(
      (t) => t.project._id === ui.currTodoProject._id
    ), [todolist, ui.currTodoProject._id]);
    
    ...
    
    const Todolist = () => {
      const todolist = useSelector((state) => state.todolist);
      const projects = useSelector((state) => state.todolistProjects);
      const ui = useSelector((state) => state.ui);
      const dispatch = useDispatch();
    
      const [deletedTodo, setDeletedTodo] = useState({});
    
      useEffect(() => {
        dispatch(changeTodoProject(projects[0]));
      }, []);
    
      const currTodos = todolist.filter(
        (t) => t.project._id === ui.currTodoProject._id
      );
      
      ...
    
      return (
        <div id="todolist" onClick={handleContainerClick}>
          ...
          {currTodos.map((todo) => {
            return (
              <Todo
                todo={todo}
                key={todo._id}
                showDeleteDialog={showDeleteDialog}
                setDeletedTodo={setDeletedTodo}
              />
            );
          })}
          ...
        </div>
      );
    };
    
    export default Todolist;