Implementing a useMap React Hook
Ever since the introduction of Hooks into the React library, creating cleaner, reusable components has become much easier. Instead of using render props and higher-order components, Hooks provide us a way to share stateful logic across multiple components without making any modifications to an application's component hierarchy. React comes with several built-in Hooks for handling component state and lifecycle, such as useState and useEffect , that can be composed to create all kinds of different Hooks. Creating your own custom Hooks can be tricky. Once you decide upon the part of a component to extract out into a separate function as a custom Hook, you need to carefully refactor this stateful logic to... For example, a Hook that manages authentication should... Keeping the Hook simple, broad and flexible opens up the components that can use it. In this case, any component that depends on the user's authentication status, such as a navigation bar to determine whether or not to display a login button or a dropdown toggle, can call this Hook and have access to its state and methods, all with a single line. When done properly, you can even abstract your Hooks into a React Hooks library for other developers to use in their own applications (irrespective of business logic). Below, I'm going to show you how to implement a custom React hook, useMap , within a React Hooks library. The useMap Hook wraps around a Map object and mimics it's API. This allows your React components to leverage Map objects without having to worry about low-level details like try...catch error handling. To get started, clone this React Hooks library template from GitHub: This template has ESLint, TypeScript and Jest already configured so that we can write, lint, test and build the useMap Hook for distribution. Within the src directory, create a useMap.ts file, which will contain the source code of the useMap Hook. Unlike plain JavaScript objects, Map objects can set objects as keys. This is quite useful for situations like mapping DOM elements to a corresponding piece of data or functionality. Additionally, Map objects are iterable, and keys set on the Map object will not conflict with properties inherited from the Object prototype like toString and constructor . The useMap Hook's API will closely follow the original React Hooks API of returning a pair of values: the current state and action methods for updating it. The Hook will return a Map object as the state and the following action methods for interacting with this object: Each time we perform any of these actions on the Map object, we create a new instance of a Map object to make it immutable to change. Here's how a component would call this Hook: Note : In the above code, the Map object maps DOM elements to strings. Let's start by implementing the Hook's state and setValue action method: ( src/useMap.ts ) The Hook can be passed a Map object or an array of key-value pairs (entries) to initialize the keys and values of the map state variable. If nothing is passed to the Hook, then initialState is set to a new, empty Map object by default. Define the clear action method, which empties the Map object of its key-value pairs: ( src/useMap.ts ) Define the set action method, which adds a new key-value pair or replaces an existing key-value pair: ( src/useMap.ts ) Define the deleteByKey action method (publicly exposed as delete ), which deletes a key-value pair: ( src/useMap.ts ) Define the initialize action method, which initializes map to a new Map object based on a passed tuple (or other iterable object). ( src/useMap.ts ) If any of these action methods are passed to a child component via props, then anytime map changes and causes the calling parent component to re-render, this triggers a re-render of its child components even though none of these methods changed. To prevent this unnecessary re-render, we should wrap each action method with useCallback and memoize the actions object with useMemo , like so: ( src/useMap.ts ) Since the keys and values of the map state variable can be of any type, they should be generically typed . Let's denote the generics by the type variable <K, V> : K for a key and V for a value. This type variable preserves the type information of the key-value pairs. Let's make use of this type variable by defining a type MapOrEntries that describes... ( src/useMap.ts ) Rewrite the useMap function as a generic function and annotate the initialState and mapOrTuple parameters with this type, like so: ( src/useMap.ts ) Finally, we need to define a type for the array, consisting of the map state variable and the action methods, returned by the Hook. Name this type UseMap , and define it as the following: ( src/useMap.ts ) Now, define the UseMapActions type, which provides a type definition for each action method: ( src/useMap.ts ) The Dispatch type tells TypeScript that setValue is a function that returns nothing. This is perfectly valid because this method simply updates the map state variable. That's it. The SetStateAction type tells TypeScript that setValue can be passed either... Altogether... ( src/useMap.ts ) Don't forget to export the types and the Hook inside the src/index.ts file! ( src/index.ts ) For a final version of this Hook, visit the GitHub repository here . Try refactoring your custom React Hooks into a separate React Hooks library, and share them with other team members and/or developers.