DEV Community

Cover image for Persisting your React state in 9 lines

Persisting your React state in 9 lines

selbekk on May 07, 2019

I was playing around with a project from Frontend Mentor this weekend, and I was implementing this theme switcher with React hooks. It struck me th...
Collapse
 
mxmzb profile image
Maxim • • Edited

Really nice, thank you Kristofer! This is a very pragmatic approach. I kind of like Redux, and you inspired me to add a construct React.useReducer on top of this. It might be just the perfect substitute for a full-blown react-redux + redux-persist setup.

Collapse
 
islami00 profile image
islami00 •

Ikr! I was also considering learning redux to persist the state left over from react query.
The main thing I was running away from was typing reducers because I use typescript. I think I'll try this with a custom json serialise function for any special objects by prepping them as classes. I'll update the design I had in mind initially with useReducer.
(Taking notes here now, haha) The reducer is a bit like redux in the sense that there's one global reducer that has a root and nodes of different ui components in the state tree. It would be generic enough to have typings and allow other components to have their own custom branch of this node.
I guess the localstate update would need to be more complex in that case to avoid chucking it all in one key. Hmm, I wonder what the limit for that is really.

Collapse
 
fredericocotrim profile image
fredericocotrim •

Could you share your approuch using usePersistedState on useReducer?

Collapse
 
jcaguirre89 profile image
Cristobal Aguirre • • Edited

I adapted both this post and this blog post by Swizec Teller to get what I wanted using useReducer. I'm making an ecommerce site and wanted to persist the cart items. So I did:

const initialState = {
  //other state
  cartItems: JSON.parse(localStorage.getItem('cart_items')) || [],
}

function reducer(state, action) {
  switch (action.type) {
    case 'UPDATE_CART': {
      const { variantId, quantity } = action.payload;
      const updatedCartItems = state.cartItems.filter(
        i => i.variantId !== variantId
      );
      const cartItems = [...updatedCartItems, { variantId, quantity }];
      // write to localStorage <-- key part
      if (typeof window !== 'undefined') {
        localStorage.setItem('cart_items', JSON.stringify(cartItems));
      }
      return {
        ...state,
        cartItems,
      };
    }

and then plugged that into context. If you need more context, I'm using gatsby and followed this guide to set up the state mgt. logic.

Hope it helps!

Collapse
 
knokit profile image
Ivo Silva •

In case you're storing falsy values, you might want to initialize the state like this:

const [value, setValue] = useState(() => {
    const storedValue = localStorage.getItem(key);
    return storedValue !== null ? JSON.parse(storedValue) : defaultValue;
});
Enter fullscreen mode Exit fullscreen mode
Collapse
 
yinonhever profile image
yinonhever •

Hey, can I use this code in a class-based component? Or can it only be used in a functional component whose state is managed with Hooks?

Collapse
 
selbekk profile image
selbekk •

Hi! Hooks unfortunately require function components to work, but you could recreate the same functionality with either a HOC or just directly in your class component. In your componentDidUpdate, add a line that serializes and saves your state to local storage.

componentDidUpdate() {
  window.localStorage.setItem(
    β€˜my state key’,
    JSON.stringify(this.state)
  );
}

When you initialize the state, you need to do the reverse:

constructor(props) {
  this.state = JSON.parse(
    window.localStorage.getItem(β€˜my state key’)
  ) || { fallback: value };
}

Hope that helps!

Collapse
 
yuhanx0728 profile image
Yuhan Xiao •

Hi @selbekk , nice article! Can you explain a little about the last section(about optimization)? How would

() => JSON.parse(localStorage.getItem(key))

inside useState() work? Wouldn't it not work, since it is declared, not invoked inside useState()?

Collapse
 
selbekk profile image
selbekk •

Thanks!

When you pass a function as an argument to useState instead of a "regular" value, it is only run on the initial render. It’s a way React offers to do initialization work only once, instead of on each render.

Collapse
 
raarts profile image
Ron Arts •

What if an Async Storage solution is being used, like AsyncStorage on react native?

Collapse
 
stephanieraymos profile image
Stephanie Raymos •

Thanks for this! Can you explain how we would actually use this within the application?

Collapse
 
selbekk profile image
selbekk •

Hi Stephanie!

You would copy the source code into your project, and import the usePersistentState hook where you would want to use it

Collapse
 
codedbychavez profile image
Chavez Harris •

This is awesome!

 
selbekk profile image
selbekk •

That happens when you try to render a regular object as children. I’d have to see more of the related code to be of any assistance

Collapse
 
taranveer_singh_97ee9875a profile image
Taranveer Singh •

Thanks a lot for this brilliant code!

Collapse
 
yinonhever profile image
yinonhever •

Great custom hook, I already used it in several projects. Are you gonna make it an NPM package?

Collapse
 
selbekk profile image
selbekk •

For 9 lines of code? I’d rather just copy it. But feel free to make a package out of it yourself if you want to πŸ₯³

Collapse
 
a_m_h_gad profile image
Jad •

This was very helpful ☺️ thanks for sharing πŸ’œ