DEV Community

Klaudiusz
Klaudiusz

Posted on

Next.js persistent state with React hooks and localStorage. How to make it work?

I'm trying to create persistent state with localStorage and React hooks in Next.js SSR app and everything seems to look good but when I reload page after update my data I'm getting error:

Warning: Text content did not match. Server: "0" Client: "5

What can I do to repair that? This is my code:

//  src/utils/Store.js

import React, { createContext, useContext, useReducer, useEffect } from "react";
import lscache from "lscache";
import Reducer, {initialState as defaultState} from "../utils/Reducer";
const StoreContext = createContext();

const initialState = lscache.get('state') ? lscache.get('state') : defaultState;

 export const StoreProvider = ({ children }) => {

  let [state, dispatch] = useReducer(Reducer, initialState);

  useEffect(
    function saveStateToLocalStorage() {
      lscache.set('state', state) 
    },
    [state]
  );

  const updateStateFromLocalStorage = () => {
    const newState = lscache.get('state') ? lscache.get('state') : defaultState;
    dispatch({ type: "updatefromlocalstorage", newState });
  };

  useEffect(function watchForChanges() {
      window.addEventListener("storage", updateStateFromLocalStorage);
    return () => {
      window.removeEventListener("storage", updateStateFromLocalStorage);
    };
  }, []);

  return (
    <StoreContext.Provider value={{ state, dispatch }}>
      {children}
    </StoreContext.Provider>
  );
};

export const useStore = () => useContext(StoreContext); 


Working example in codesandbox:

Edit intelligent-lamport-s0hu8

Top comments (2)

Collapse
 
xstevenyung profile image
Steven Yung • Edited

Hello Klaudiusz!

I found your thread on Google trying to solve mine πŸ˜‚

I don't have a similar issue but I successfully did something similar, I figure, it's better that I post it than not at all.

export const PostsContextProvider = ({ children, storageKey = "posts" }) => {
  const [isInitialized, setIsInitialized] = useState(false);

  const [posts, dispatchPost] = useReducer(reducer, []);

  useEffect(() => {
    if (isInitialized) {
      localStorage.setItem(storageKey, JSON.stringify(posts));
    }
  }, [posts]);

  useEffect(() => {
    dispatchPost({
        type: SET_POSTS,
        value: JSON.parse(localStorage.getItem(storageKey)) || []
      });

    setIsInitialized(true);
  }, []);

  return (
    <PostsContext.Provider value={{ posts, dispatchPost }}>
      {children}
    </PostsContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

As you can see, my version use 2 useEffect. One to initialized the data using the localStorage if it's there is something (otherwise, it defaults to []) and one to keep the localStorage updated with the app.

Happy to answer question if you have some πŸ‘

Collapse
 
helpbot profile image
Experimental Help Bot

To fix the issue you're experiencing, you can update the updateStateFromLocalStorage function to use the newState parameter passed to it from the storage event listener, instead of trying to retrieve the state from localStorage again. This will ensure that the correct state is passed to the reducer and the correct value is displayed on the page.

Here's an updated version of the StoreProvider component that uses this approach:

const StoreProvider = ({ children }) => {
  let [state, dispatch] = useReducer(Reducer, initialState);

  useEffect(
    function saveStateToLocalStorage() {
      lscache.set('state', state) 
    },
    [state]
  );

  const updateStateFromLocalStorage = (event) => {
    if (event.key === "state") {
      const newState = JSON.parse(event.newValue);
      dispatch({ type: "updatefromlocalstorage", newState });
    }
  };

  useEffect(function watchForChanges() {
      window.addEventListener("storage", updateStateFromLocalStorage);
    return () => {
      window.removeEventListener("storage", updateStateFromLocalStorage);
    };
  }, []);

  return (
    <StoreContext.Provider value={{ state, dispatch }}>
      {children}
    </StoreContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

This updated version uses the newValue property of the storage event to get the updated value of the state key in localStorage, and it passes this value to the reducer when the updatefromlocalstorage action is dispatched. This ensures that the correct value is used for the state variable and that it is displayed on the page.

I hope this helps! Let me know if you have any other questions.


I'm an experimental help bot that leverages ChatGPT. As such, the answers I provide may be incorrect, incomplete, or even nonsensical. I am not associated with OpenAI.

Please reply to my comment(s) with your own corrections and feedback.