Hooks in practice : system of favorites in localstorage

Create your first hooks and reuse them in your React apps

French version : Les hooks par la pratique : mettre en place un sytème de favoris

Goals

  • Find out how to add hooks to our React components
  • Write easy re-usable logic
  • Use the localstorage API

You can test the application and see the sources of this tutorial.

Context

The hooks were introduced by the developers of React to allow a better organization of the code by exporting certain elements of component logic. Several hooks are developed and used within a project, and the simplicity with which they can be re-used make it a tool staple of the React ecosystem.

A bit of theory

We are going to set up a hook step by step to manage a system of favorites. Our application manages users, and we want to be able to bookmark certain users, remove them from our favorites and easily find.

For this we will use the localstorage. Each user has a unique ID, our favorites will be stored in the form of an array containing IDs, saved as a character string in the localstorage.

Start the project

We will use the create-react-app tool to not worry about the project setup and focus on the code.

Node.js and npm should be installed, then the following command executed to create the project:

npx create-react-app bookmark-hook-tutorial

Creation of our components

A first component will display a user:

// User.js
{usersState.map((user) => {
  return (
    <div key={user.id} className={`user`}>
      <div>{user.username}</div>
    </div>
  );
})}

The users will then be listed by another component:

// Users.js
function Users({ users }) {
  return (
    <div className={"root"}>
      <div className={"users"}>
        {usersState.map((user) => {
          return (
            <div key={user.id} className={`user`}>
              <div>{user.username}</div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

Finally the list of users is rendered in our application, by retrieving a local list of users:

// App.js
function App() {
  return <Users users={users} />;
}

Setting up the hook

In our example we want to manage favorites. We want to be able to add, delete a favorite, consult the list of favorites, and find out if a user is favorite or not.

// useBookMarks.js
function useBookmarks() {
  const [bookmarks, setBookmarks] = useState(() => {
    const ls = localStorage.getItem("bookmarks");
    if (ls) return JSON.parse(ls);
    else return [];
  });

  const toggleItemInLocalStorage = (id) => () => {
    const isBookmarked = bookmarks.includes(id);
    if (isBookmarked) setBookmarks((prev) => prev.filter((b) => b !== id));
    else setBookmarks((prev) => [...prev, id]);
  };

  useEffect(() => {
    localStorage.setItem("bookmarks", JSON.stringify(bookmarks));
  }, [bookmarks]);

  return [bookmarks, toggleItemInLocalStorage];
}

Using our hook in our component

All we have to do is use the logic of our hooks within our components. For the User component, we add the functionality of adding / deleting favorites :

// User.js
{usersState.map((user) => {
  const isBookmarked = bookmarks.includes(user.id);
  return (
    <div key={user.id} className={`user ${isBookmarked ? "bookmarked" : ""}`}>
      <div>{user.username}</div>
      <button onClick={toggleBookmark(user.id)}>
        {isBookmarked ? "Remove from bookmarks" : "Add to bookmarks"}
      </button>
    </div>
  );
})}

For the list of users, we add a filter functionality, and a little styling :

// Users.js
function Users({ users }) {
  const [bookmarksOnly, setBookmarksOnly] = useState(false);
  const [usersState, setUsersState] = useState(users);
  const [bookmarks, toggleBookmark] = useBookmarks();

  const changeBookMarksOnly = (e) => {
    setBookmarksOnly(e.target.checked);
  };

  useEffect(() => {
    setUsersState(users.filter((s) => (bookmarksOnly ? bookmarks.includes(s.id) : s)));
  }, [users, bookmarksOnly, bookmarks]);

  return (
    <div className={"root"}>
      <label htmlFor="check">Bookmarked users only</label>
      <input id={"check"} type="checkbox" value={bookmarksOnly} onChange={changeBookMarksOnly} />
      <div className={"users"}>
        {usersState.map((user) => {
          const isBookmarked = bookmarks.includes(user.id);
          return (
            <div key={user.id} className={`user ${isBookmarked ? "bookmarked" : ""}`}>
              <div>{user.username}</div>
              <button onClick={toggleBookmark(user.id)}>
                {isBookmarked ? "Remove from bookmarks" : "Add to bookmarks"}
              </button>
            </div>
          );
        })}
      </div>
    </div>
  );
}

Our favorites system is operational, and it persists even after refreshing or closing the application thanks to localstorage.

Our components are little impacted by the localstorage backup logic, and our hook can easily be re-used in another application.

You can find all the sources here and a demo of the application