Using Custom React Hooks for Persistent User Data

Using Custom React Hooks for Persistent User Data

Custom Hooks

Custom hooks are like any React hook that can abstract some interactive feature from your component and make it a reusable function. Since everything is JavaScript in react we can simply use every power JavaScript gives us. That said we use react's built in hooks for things like reactive state management, watching some changes and making some side-effects depending on those changes etc. That means we need to know react hooks really well in order to get the best out of custom hooks. But I won't go into detailed explanation of react hooks since react's official documentation is great. You should check it out if you haven't already. And also Kent C. Dodds has really cool content about hooks available in epicreact.com.

We Start to Build Our Application

Let's say we have an arbitrary application, where users enter their favorite stuff like beverages, food, movies. And we need to make separate pages for every stuff in the application, not to confuse our users. Your product owner wants a favorite movie page in which a user enters his/her favorite movies and adds them to the list. Easy! You make a simple page with an input box and a list of items.

function FavoriteMovies() {
  const [favoriteMovies, setFavoriteMovies] = useState([]);
  const [movieName, setMovieName] = useState('');

  function addFavoriteMovie() {
    setFavoriteMovies([
      ...favoriteMovies,
      {
        id: generateId(),
        name: movieName
      }
    ])
  }

  return (
    <div>
      <ul>
        {favoriteMovies.map(favoriteMovie => (
          <li key={favoriteMovie.id}>{favoriteMovie.name}</li>
        ))}
      </ul>
      <input
        type="text"
        value={movieName}
        onChange={(e) => setMovieName(e.target.value)}
      />
      <button onClick={addFavoriteMovie}>Add To Favorite</button>
    </div>
  )
}

You implement this simple page lightning fast and show it to your product owner. And she loves it! But she wants users to see a list of their favorite beverages in a different page, only difference is they want a checkbox if the beverage is non-alcoholic or not, and she asks you to implement that too. You, of course implement that easily.

Things Get A Little Messy

While everything seems perfect, your product owner comes in a rush and says, users lose their favorite stuff when they leave the page or refresh it. And also wants you to implement a favorite animals page, and gives you a list of animals to fill the select list. While you implement the FavoriteAnimals page you also add local-storage support to the page so users don't lose their data if they close or refresh the page.

function FavoriteAnimals({ animalsToSelectFrom }) {
  const [favoriteAnimals, setFavoriteAnimals] = useState(() => JSON.parse(window.localStorage.getItem('favoriteAnimals')) || []);
  const [animalName, setAnimalName] = useState('');

  useEffect(() => {
    window.localStorage.setItem('favoriteAnimals', JSON.stringify(favoriteAnimals));
  }, [favoriteAnimals]);


  function addFavoriteAnimal() {
    setFavoriteAnimals([
      ...favoriteAnimals,
      {
        id: generateId(),
        name: animalName
      }
    ])
  }

  return (
    <div>
      <ul>
        {favoriteAnimals.map(favoriteAnimal => (
          <li key={favoriteAnimal.id}>{favoriteAnimal.name}</li>
        ))}
      </ul>
      <select
        value={animalName}
        onChange={(e) => setAnimalName(e.target.value)}
      >
        {animalsToSelectFrom.map(animal => (
          <option key={animal.name} value={animal.name}>{animal.name}</option>
        ))}
      </select>
      <button onClick={addFavoriteAnimal}>Add To Favorite</button>
    </div>
  )
}

After your job is done with the FavoriteAnimals page, you copy paste the logic which implemented for local-storage to other pages (ALERT). Product owner seems very happy with the end result but confused about the implementation time, she thinks it takes more time every time she wants a change. After awhile she comes with a rush, again. She says our customer can't see the information they enter if they open the application in a different browser. She says this needs to be fixed really quick, and also adds that our users needs a favorite food page.

Custom Hooks Comes to Rescue

And you implement the food page and backend services for CRUD operations for every page. And then you copy paste the logic from food page to every page you've written. And you see your product owner coming and going, asking about the issue. While you struggle to implement the very exact logic to every page you get bugs all around because you copied it wrong or made some typos. You say that it's enough (finally), and decide to abstract that logic and use it in every page. And decide to go with the custom hook abstraction, since you already use hooks everywhere in the application it should be really easy for you. You write your own custom hook proudly;

function usePersistentFavorites({
  initialValue = [],
  url
}) {
  const [favorites, setFavorites] = useState(initialValue);

  useEffect(() => {
    fetchFavorites();
  }, []);

  useEffect(() => {
    axios.post(url, favorites);
  }, [favorites]);

  async function fetchFavorites() {
    const response = await axios.get(url);
    setFavorites(response.data);
  }

  return [favorites, setFavorites]
}

And use it in your application;

function FavoriteAnimals({ animalsToSelectFrom }) {
  const [favoriteAnimals, setFavoriteAnimals] = usePersistentFavorites({
    url: '/favorite-animals'
  });
  const [animalName, setAnimalName] = useState('');

  function addFavoriteAnimal() {
    setFavoriteAnimals([
      ...favoriteAnimals,
      {
        id: generateId(),
        name: animalName
      }
    ])
  }

  return (
    <div>
      <ul>
        {favoriteAnimals.map(favoriteAnimal => (
          <li key={favoriteAnimal.id}>{favoriteAnimal.name}</li>
        ))}
      </ul>
      <select
        value={animalName}
        onChange={(e) => setAnimalName(e.target.value)}
      >
        {animalsToSelectFrom.map(animal => (
          <option key={animal.name} value={animal.name}>{animal.name}</option>
        ))}
      </select>
      <button onClick={addFavoriteAnimal}>Add To Favorite</button>
    </div>
  )
}

As you finish implementing your custom hook, you confidently maintain the application with new requests from your users. They may want you to implement a hybrid logic which uses both local-storage and database for these pages. And it wouldn't be a problem for you since all your shared logic is abstracted to a custom hook.

Use Your Imagination

useYourImagination() This is how React's document about custom hooks finish. And I couldn't agree more. Custom Hooks opens a whole new world for abstraction in your applications. Use its power to share logic between your components, or just to abstract some functionality away.