One of the things we love about programming is the ability to take code, place it in a function, and reuse it in other places in the software. Let’s imagine a scenario where we want to share our
localStorage code with other components so other components could synchronize state with
In the process, we’ll learn a few of the conventions for doing this and we’ll learn about some additional challenges that come with generalizing our code. As always, follow AHA programming practices!
Kent C. Dodds: [0:00] The logic that we have here for storing some state into localStorage and keeping it synchronized could be useful in other areas of our application.
[0:30] We need to generalize the code that's in our function. Instead of a name, it might make more sense to call this state and setState. Instead of getting the item with the string name, it might make more sense for the user of this function to provide us a key for localStorage, so we'll accept a parameter called key.
[0:51] Instead of this as a default value, it might make sense for people to provide us their own default value. We'll accept that as another parameter. We can default that to an empty string just in case they don't want to provide it.
[1:05] Then we'll replace that with the default value. We'll replace the string name with a parameter that we accept called key. Then let's make sure we update these two references to that old state value as well.
[1:19] Then when we call useLocalStorageState, we're going to need to get access to that stateUpdater function and to the state value itself. Let's return state and setState. We can make our custom Hook have a similar API to useState so it's familiar to people who are used to the useState Hook.
[1:39] Then we can come down here. We can get our name and our setName from useLocalStorageState. We'll specify our key as name. Then we're done. That's a pretty straight-up refactor from the code that we had before into a custom function. If we type in here Chuck and then refresh, we'll see that Chuck is still in there.
[2:02] One thing I want to call out here is that we prefaced our function with the word use. That's a convention and not required. We could change this to whateverWeWant. Save that. Things will work just as well. We can hit refresh. There we have it.
[2:19] The reason that we follow this convention is because the eslint-plugin-react-hooks is able to enforce some of the same recommendations and rules on our custom Hooks as it is on the built-in Hooks because it acts under the assumption that functions that begin with use are custom Hooks.
[2:59] We also learned the importance of using the use prefix on our custom Hooks so that we follow the community convention and the ESLint plugin can help us avoid mistakes.
[3:09] Incidentally, one such mistake is happening right now. In this index.html file, I can't have ESLint running to show me the mistakes that I'm making. I made a mistake right here in this dependency array. I wonder if you can figure out what it is. I'm missing one of the dependencies for my callback.
[3:28] Before, we had this key hardcoded as name. It couldn't ever change. My dependency array didn't need to include that. Now, I'm accepting a key as a parameter from the users of the LocalStorageState custom Hook. They could potentially change that value. If they do, then I need to make sure to respond to that change so that I update local storage and not lose any of the user's work.
[3:53] It's important that in your applications you use and follow that ESLint rule so you can avoid bugs like this. With this in mind, we could make this useLocalStorageState Hook a lot more robust by removing the item at the old key if that key has changed. I'm going to leave that as a fun exercise for you to do later.
Question about the exercise left to the reader (remove old key from localStorage): It first seemed to me calling our custom hook from Greeting with a changing key doesn't make sense. But then yes Greeting could get that key 'name' from its props. Greeting could get its key prop being "firstName", then later on after some change in its parent that would need to greet the user by lastName instead, its key prop could become "lastName". Well ok, then Greeting must become linked to lastName in localStorage. But ... what is the sense to delete firstName from localStorage at that moment just because perhaps temporarily we greet by last name? And what if firstName in localStorage is used from another part of the app? That is why I don't understand.
An interesting exercise is to add a button (as in previous examples) and a counter which increments when you click the button, with state stored using the new useLocalStorageState hook. Does it work as expected? How can you fix it?