For the past several weeks I’ve continued to explore the impact of React Hooks, an upcoming feature that is accessible via version 16.7.0-alpha.2. The two major improvements to React applications I’ve noticed are:
- More simplicity, including less overall lines of code
- Less code repetition, enabling adherence to the DRY (don’t repeat yourself) principle
- Ability to modularize behaviors
Indeed, the React Docs emphasize that the motivation for Hooks included the need for easing the reuse of stateful logic inside React components. Prior to the introduction of Hooks, you’d often find yourself copy-pasting chunks of stateful logic between components. Copying and pasting code can (and should) feel wrong to any developer. Each time you paste that same code, you increase the number of locations you must return to in order to change that bit of logic.
The React library provides several Hooks for you to use; I like to think of these as the fundamental Hooks from which all others are defined. You can find a list of them here. Hooks become interesting when you start composing new ones. I’m sure we’ll soon see (if not already) some libraries being published containing all sorts of custom Hooks for different use cases. In order to practice using the new feature, I’ve begun creating my own set. I wanted to go through the creation of one them here.
Hook Rules and Conventions
The useSomething Convention
Before I get started with implementing a custom hook, there are a couple of rules and conventions to consider. If you look at the list of basic React Hooks provided by the library, you’ll notice they all begin with the word use. This should be followed when creating new Hooks as well; not only is it the convention, but the linter plugin created by the React team looks for these functions in order to make sure your code follows the Hooks’ rules as well. The naming makes sense, too. Say you’re using the useState hook; in this case you are using React’s state-handling functionality. In useContext you are using React’s Context API functionality. With custom hooks, you define what behaviors you’re using.
Rules for Using Hooks
The React Docs’ rules regarding hooks are pretty much all about where you call Hooks. The basic idea is that for each component rendering, you want your hooks to be called in the same order every time. The way the Hooks system is implemented demands this; it’s how the state is maintained between all of your hook calls.
Note: this seems to be one of the more controversial design decisions. See the Docs for more details.
So the rules are:
- Keep your Hooks calls at the top level — keep them out of loops, conditional blocks, and nested functions
- Keep your Hooks calls inside React functions and/or inside custom Hooks functions.
These rules and conventions will keep your code easy-to-read by maintaining a consistent place for stateful declarations to be made.
With rules and conventions out of the way, we can get started constructing a custom hook.
Creating the React Hook
Lets say you’re building a React application that has lots of HTML select elements. Naturally, you want to keep track of the selected option inside each element. If you’re using a single React component for all of your select elements, there’s no issue. You can write your logic to keep track of its state in one place and everybody is happy. But in our case, say we have a select element inside our root App component as well as inside a custom Selector component.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const app = () => { return ( <div> <Selector /> <select> <option>Tokyo</option> <option>London</option> <option>NYC</option> </select> </div> ); }; const Selector = () => { return ( <div> <select> <option>Red</option> <option>Blue</option> <option>Yellow</option> </select> </div> ); }; |
Our goal is to keep track of the current selected option and rerender components when they change. In pre-Hooks React, we’d have to immediately convert these into class components so we could use setState in an event handler.
Another option would be to use the basic hooks afforded to us in the new React library. This would be easy; we could just make calls to useState in each component and configure accordingly.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
const app = () => { const [option, setOption] = useState("Tokyo"); function handleChange(e) { setOption(e.target.value); } return ( <div> <Selector /> <select onChange={handleChange}> <option>Tokyo</option> <option>London</option> <option>NYC</option> </select> </div> ); }; const Selector = () => { const [option, setOption] = useState("Red"); function handleChange(e) { setOption(e.target.value); } return ( <div> <select onChange={handleChange}> <option>Red</option> <option>Blue</option> <option>Yellow</option> </select> </div> ); }; |
But as you can see, we’re using the same logic in two different places, so there’s an even better option: create a custom Hook that uses useState and some other basic Hooks to isolate this logic into a single, reusable function (hook).
This custom Hook is a great example because it makes use of three rudimentary React Hooks: useState, useEffect, and useRef.
We use useState because we want to use React’s state-handling functionality to keep track of the selected option in each select element.
Instead of placing our event-handling logic in React lifecycle methods, we use useEffect to subscribe to and unsubscribe from the select elements.
Finally, we need a way to reference the select element we’re keeping track of, so we’ll make use of useRef.
Here’s the code for the custom hook, useSelection. Note that it returns an object containing the ref attribute, as well as the option attribute.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function useSelection() { const ref = useRef(null); const selector = ref.current; const [selectedOption, setSelectedOption] = useState(); useEffect(() => { const onChange = e => { setSelectedOption(e.target.value); }; if (selector) { selector.addEventListener("change", onChange, false); setSelectedOption(selector.value); return () => { selector.removeEventListener("change", onChange, false); }; } }); return { ref: ref, option: selectedOption }; } |
Here’s the root App component utilizing the hook:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const app = () => { const selection = hooks.useSelection(); return ( <div> <select ref={selection.ref}> <option>Tokyo</option> <option>London</option> <option>NYC</option> </select> <p>Selection: {selection.option}</p> </div> ); }; |
You can use a separate useSelection hook for as many select elements as you need; you just have to be sure to pass the reference in a ref attribute in the select tag as seen above.
Again, in pretty much every case, you’ll have a single React component to handle the logic for all your select elements, but this example shows a more complex custom React hook that makes use of several of the basic hooks.
Conclusion
I think custom React hooks might be the most attractive part of these new features. As I said before, there will probably be entire libraries of custom Hooks written for developers to use, and I encourage you to try creating your own. Creating this Hook in particular helped me further understand several React concepts. For some extra points, I’m providing a couple more of my custom Hooks below. Thanks for reading!
A Couple More Custom Hooks
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import React, { useState, useEffect, useRef } from "react"; function usePointerLocation() { const [x, setX] = useState(0); const [y, setY] = useState(0); useEffect(() => { const onMouseMove = e => { setX(e.pageX); setY(e.pageY); }; window.addEventListener("mousemove", onMouseMove, false); window.addEventListener("mouseenter", onMouseMove, false); return () => { window.removeEventListener("mousemove", onMouseMove, false); window.removeEventListener("mouseenter", onMouseMove, false); }; }); return { x, y }; } function useClick() { const [clicked, setClicked] = useState(false); const [location, setLocation] = useState({ x: 0, y: 0 }); useEffect(() => { const onClick = e => { setClicked(!clicked); setLocation({ x: e.pageX, y: e.pageY }); }; window.addEventListener("click", onClick, false); return () => { window.removeEventListener("click", onClick, false); }; }); return { clicked, location }; } export default { usePointerLocation, useKeyPress, useClick, useSelection }; |