skip to Main Content

Here is a small React app that is using State and saves updated data to localStorage.
You can create buttons by inputting the name and pressing Add button.

import "./styles.css";
import { useState } from "react";

export default function App() {
  const [buttonlist, setButtonList] = useState([]);
  const [newName, setNewName] = useState("");
  const [storeChecked, setStoreStatus] = useState(false);

  // local storage
  if (!storeChecked) {
    if (localStorage.getItem("ButtonList")) {
      console.log("store is available");
      let newToDoObject = localStorage.getItem("ButtonList");
      console.log(JSON.parse(newToDoObject));
      setStoreStatus(true);
      setButtonList(JSON.parse(newToDoObject));
    } else {
      console.log("store is NOT available");
      localStorage.setItem("ButtonList", JSON.stringify(buttonlist));
    }
  } else {
    // this update works while rerendering
    //localStorage.setItem("ButtonList", JSON.stringify(buttonlist)); // <-- line 23
  }

  console.log("rerendered buttonlist:", buttonlist);

  const ButtonList = ({ button }) => {
    return (
      <button className="button" value={button.name}>
        {button.name}
      </button>
    );
  };

  const handleListChange = (event) => {
    console.log("target value", event.target.value);
    setNewName(event.target.value);
    console.log("new name:", newName);
  };

  const addButtonList = (event) => {
    console.log("add to list:", newName);
    const buttonListObject = {
      name: newName,
      id: Math.floor(Math.random(5) * (10000 - 1000)),
      tasks: [],
    };
    event.preventDefault();
    setButtonList(buttonlist.concat(buttonListObject));
    // this update is always late
    localStorage.setItem("ButtonList", JSON.stringify(buttonlist)); // <-- line 52
    console.log(buttonlist);
  };

  return (
    <div className="App">
      <h2>Button List</h2>
      {buttonlist.map((button) => (
        <ButtonList key={button.id} button={button} />
      ))}

      <form id="create_list" onSubmit={addButtonList}>
        <div>
          Name:{" "}
          <input
            onChange={handleListChange}
            value={newName}
            id="name_field"
            maxLength={60}
          />
        </div>
        <div>
          <button type="submit" className="actionbutton">
            Add
          </button>
        </div>
      </form>
    </div>
  );
}

Button list

Now there are two points where the localStorage is updated:

  1. addButtonList function, line 52. This is run when the Add button is clicked. It also sets the state of the button list.

  2. in the if / else statement, line 23 (commented out)

Updating at point 1. is always late, while point 2. works fine.

I couldn’t get the localStorage visible in Codesandbox but if you refresh the app,
you’ll see that the created buttons are shown, except the last one you created.

So my question is, what is the right way to store the state data in an app like this? Is the point 2. ok? I found an article that suggested using useEffect for handling the up-to-date data.

2

Answers


  1. Quoting from the documentation:

    Every React component goes through the same lifecycle:

    • A component mounts when it’s added to the screen.
    • A component updates when it receives new props or state, usually in response to an interaction.
    • A component unmounts when it’s removed from the screen.

    The lifecycle of an Effect

    Now you can find yourself that what would be most ideal event to serialise or store states in an App.

    mount – definitely no

    updates – may be required if your app requires a very high data availability, in the sense, even the latest state update to be available even in the event of an unforeseen and an unfortunate crash of the app.

    However unmount may be the most balanced option between performance and data availability. Please be noted that in this case the data will serialise only once – during the time the component is unmounted.

    Now when it comes to implementing the same, useEffect is an excellent escape hatch provided by React. As you know, localStorage is not mapped into React. Whenever we encounter something which is not available in React, we need to step out from React. useEffect hook is the way to do that. It solely meant to provide the codes to synchronise two systems – surely one is React and the other one may be a non-React system like localStorage.

    Therefore a sample code below would do the job we have been discussing.

    Please do note that the empty array given as its dependency means that the code inside the hook will be invoked on mount as well as unmount of the component. It will not be invoked for the state updates.

    useEffect(() => {
      if (buttonlist.lenghth == 0) {
        // the app has just been mounted.
        // therefore check the local store,
        // and load the state from it.
    
        let newToDoObject = localStorage.getItem('ButtonList');
        if (newToDoObject) {
          setButtonList(JSON.parse(newToDoObject));
        }
      } else {
        // the app has some state to serialise,
        // therefore it means it is in unmount state now,
        // so serialise the state here.
        localStorage.setItem('ButtonList', JSON.stringify(buttonlist));
      }
    }, []);
    
    Login or Signup to reply.
  2. Either place is correct (or rather they are not incorrect), assuming you understand the React component lifecycle and values you are working with.

    Current Implementation:

    • Line 52 doesn’t work because React state updates are not immediately processed, they are enqueued and asynchronously processed by React and trigger a component rerender. There is also a Javascript closure over the current buttonList state value in the addButtonList callback that prevents it ever seeing any updated state value. The buttonList you update to localStorage is the un-updated current state value.
    • Line 23 works, but for completely wrong reasons. It’s an unintentional side-effect to update the localStorage when the component renders and the proper conditions are met, e.g. that storeChecked condition. Unintentional side-effects are a React anti-pattern and should be avoided.

    Suggestions:

    • Updating localStorage from the callback:

      Create a new state value in the callback scope and use that to both enqueue the state update and update localStorage. Array.concat creates a new array reference with the argument appended to the source array. Use a lazy initializer function to initialize the buttonList state from localStorage.

      const [buttonlist, setButtonList] = useState(() => {
        return JSON.parse(localStorage.getItem("ButtonList")) || [];
      });
      
      ...
      
      const addButtonList = (event) => {
        event.preventDefault();
      
        const buttonListObject = {
          name: newName,
          id: Math.floor(Math.random(5) * (10000 - 1000)),
          tasks: [],
        };
      
        // Create the next state value
        const nextButtonList = buttonlist.concat(buttonListObject);
      
        // Update state and localStorage at the same time
        setButtonList(nextButtonList);
        localStorage.setItem("ButtonList", JSON.stringify(nextButtonList));
      };
      
    • Using a useEffect hook to intentionally issue the side-effect to update localStorage:

      Use a lazy initializer function to initialize the buttonList state from localStorage.

      const [buttonlist, setButtonList] = useState(() => {
        return JSON.parse(localStorage.getItem("ButtonList")) || [];
      });
      
      // Persist state updates to localStorage
      useEffect(() => {
        localStorage.setItem("ButtonList", JSON.stringify(buttonList));
      }, [buttonList]);
      
      ...
      
      const addButtonList = (event) => {
        event.preventDefault();
      
        const buttonListObject = {
          name: newName,
          id: Math.floor(Math.random(5) * (10000 - 1000)),
          tasks: [],
        };
      
        // Enqueue state update using current state value
        setButtonList(buttonList => buttonlist.concat(buttonListObject));
      };
      

    Between the two options the second is preferred because:

    1. It allows addButtonList to use a functional state update, e.g. correctly update from the current state value versus whatever value buttonlist might have in the callback function scope.
    2. Improves separations of concerns. Updating state is decoupled from persisting it elsewhere.
      • addButtonList should only update the buttonList state by adding
        to it
      • The useEffect is specifically for persisting buttonList
        state changes to localStorage. Any other handlers that update the
        buttonList state no longer also need to manually persist it, it’s
        handled "automagically" by this component and effect.
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search