What are React Hooks?
First of all, React Hooks don’t add new functionality. Everything you can do with Hooks you can also achieve without them by using the “older” way of React class components. That being said, React Hooks allow you to write cleaner, more maintainable code without the boilerplate code of React Class Components.
With React Hooks you can manage a Component’s life cycle and state in Functional Components instead of using methods such as componentDidMount()
, componentDidUpdate()
and componentWillUnmount()
that are only available when extending React.Component
. A React hook is a function that must be prefixed with “use”.
If you are familiar with Design Pattern principles: React Hooks rely on composition instead of inheriting from a React class. Composition is considered a more flexible design than inheritance.
What do I need to use Hooks?
To use React hooks you need at least React version 16.8.
The three most commonly used React hooks are
useState
: a hook to manage component internal stateuseRef
: a hook to access a component’s DOM elementuseEffect
: a hook to run functions on Component initialization and destruction
But there are more:
useContext
useReducer
useCallback
useMemo
- and even some more
Then there is the possibility to create your own custom hooks that build upon any of the ones above.
Linting rules
There are some rules or caveats that you have to keep in mind when using hooks:
- Only call hooks at the top level of your React Functions, not from regular functions, because React relies on the order in which Hooks are called. The in-depth reason can be read on the official doc page.
- Don’t call hooks inside loops, conditions or nested functions
If we want to run an effect conditionally, we can put that condition inside our Hook:
useEffect(function persistForm() { if (name !== '') { localStorage.setItem('formData', name); } });
You can install linting rules to enforce that:
npm install eslint eslint-plugin-react react-hooks --save-dev
useState
To access state in Functional Components you can make use of useState
. This is the equivalent of using this.state = {}
and this.setState()
in React Class Components.
Here we are setting the input value as state and display it in a p
tag.
// Step 1: Import useState const { useState } = React; const App = () => { // Step 2: Specify state name, setter method and default value const [name, setName] = useState("Default value"); return <div> // Step 3: Use setter method <input type="text" onChange={(evt) => { setName(evt.target.value); }}/> // Step 4: Display state <p>Name is: {name}</p> </div> } ReactDOM.render(<App />, document.getElementById("root"));
useRef
A ref does more than just reference an HTML element. A ref…
- can be used to reference an HTML element
- can be used to store a value that’s stable between renders, i.e. state that is not rendered or does not change
- can mutate the ref’s value directly
- does not cause a re-render when they change
These characteristics let you use refs as “instance variables” in functional components for example to:
- Keep data between renders
- Storing a previous value
- Track if component is mounted
- Hold HTTP request cancel token
- Reference a 3rd party library instance
- Debounce a call / declare local cache
- Store flag that something happened
- Store value used in useEffect
Here we set the value of an input field to “Test” every time we hover over the input field.
const { useRef } = React; const App = () => { const inputRef = useRef(); return <div> <input type="text" ref={inputRef} onMouseOver={() => inputRef.current.value = "Test"}/> </div> } ReactDOM.render(<App />, document.getElementById("root"));
The ref
attribute on the element is specific to React. Note the property current
which references the HTML element.
useEffect
Effect here means “side effect”, not graphical effects such as animations: If Components always return the same result (e.g. rendering the same output), no matter how many times you call them, then they are called Pure Components otherwise they have side effects, such as state changes.
React’s useEffect hook allows you to cause such side effects. The syntax is as followed:
const {useEffect} = React; // or import React, {useEffect} from "react"; useEffect(() => { // I am called when my component is mounted // e.g. add event listener return () => { // I am called when my component is unmounted or before rerendering // e.g. remove event listener }}, // List variables that - when changed - rerun the effect [rerunWhenChanged] );
If the second parameter (rerunWhenChanged
)
- is null, then useEffect runs on every invocation of the component
- is an empty array, then useEffect runs only on first invocation of component
- is an array containing values, then useEffect runs only when any of those values changed
useEffect example
const {useEffect, useState} = React; const Child = () => { const [runAgain, setRunAgain] = useState(true); useEffect(() => { console.log("Child Running"); return () => {console.log("Child Completed")}; }, [runAgain]); return <p><button onClick={() => setRunAgain(!runAgain)}>Change child state</button></p> } const App = () => { const [renderChild, setRendering] = useState(true); return <div> {renderChild ? <Child /> : null} <button onClick={() => setRendering(!renderChild)}>Toggle child rendering</button> </div>; } ReactDOM.render(<App />, document.getElementById("root"));
Outputs “Child Running” initially.
Clicking “Change child state” outputs “Child Completed” and “Child Running”.
Clicking “Toggle child rendering” outputs “Child Completed”.
useContext
Let’s assume you have two components deep in your hierarchical component tree that need the same state:
Approach 1: You could each let them fetch the data separately. That works, but it comes at the cost of having two requests and changing the state in one component leaves it out of sync with the other.
Approach 2: You lift state up the component tree, meaning that the common state is handled by a root component which loads the data and then passes it down to the components that need the state. That also works, but it is tedious to pass the props down manually (Prop Drilling) and this violates the Principle of Least Privilege, meaning that some “in-between”-components would have to receive props they don’t care about.
Approach 3 (best): You define a context with the values as a root component and then use useContext
in the child components that need the context values. Let’s go through the 3 simple steps:
Step 1: Create context, preferably in a separate file.
export const MyContext = React.createContext("initialValue");
Step 2: Identify the component in your hierarchy that should act as a root and wrap it in the context provider
<MyContext.Provider value={{foo : "bar"}}> // here are the child components </MyContext.Provider>
Step 3: use useContext
in any of the child component to access the values
const context = useContext(MyContext); const value = context.foo // "bar"
useReducer
In its simplest form a reducer is a function like that:
(previousState, action) => newState
It is called reducer because it is taking many values and reduces them down to a single value, e.g. a single state object. useReducer
is very similar to useState
, because it also sets state.
In React you use useReducer
, dispatch
and actions like this:
// Define this in your react functional component import myReducer from "./myReducer" const [state, dispatch] = useReducer(myReducer, initialState, initFunction); // initFunction can be omitted if you do not need to lazily initialize state
Now when you dispatch an action like this:
// dispatch this somwhere in your component tree dispatch({type: 'someAction', data: "someData"});
your reducer
function will be invoked and can handle the data to return a new state:
// define a reducer - preferably in an external file export const reducer = (currentState, action) => { switch(action.type) { case "someAction": // use action.data in a way that returns a new state return newState; default: throw new Error("Unhandles action " + action.type) } }
Why you would use useReducer
- You extract your logic outside of the component which makes it more maintainable. For example, it is easier to check if an email address has a valid form before setting it as state
- You can reuse the same reducer in multiple components
- You can easily test reducer functions, because they are simple pure functions
- useReducer scales better than useState, because it is easier to pass down the
dispatch
function to your component tree
Full example
The following example adds the value (add
action) of an input field to a list (as state) on every keystroke. There is also a button which removes the last entry from the list (“remove” action).
const {useReducer} = React; const App = () => { function myReducer(state, action) { let newState = state; if(action.type === "add") { newState = [...state, action.data]; } else if(action.type === "remove") { newState = state.slice(0, -1); } console.log(newState); return newState; } const [myList, dispatch] = useReducer(myReducer, []); function onChange (evt) { const input = evt.target.value; if(input !== "") { dispatch({type: "add", data: input}) }; } return <div> <ul> {myList.map((entry) => <li>{entry}</li>)} </ul> <input type="text" onChange={onChange}/> <button onClick={() => dispatch({type: "remove"})}>Remove last item</button> </div>; } ReactDOM.render(<App />, document.getElementById("root"));
useCallback
const memoizedCallback = useCallback(() => { doSomething(a, b) }, [a, b]);
will return a memoized/cached version of the callback that only changes if one of the dependencies useCallback
[a, b]
has changed. To understand why and when this is necessary, you have to understand referential equality in JavaScript:
true === true // true false === false // true 1 === 1 // true 'a' === 'a' // true {} === {} // false [] === [] // false () => {} === () => {} // false const z = {} z === z // true
As you see, just because two objects (or two arrays or two functions) are created with the same content (empty in the example above) that does not mean that they are identical. In fact, each of them is pointing to a different address in memory. In other words, if you compare them they fail referential equality. The thing is, comparing referential equality is exactly what React uses to determine whether it should re-render a component or not.
Take this example:
const Parent = () => { const [counter, setCounter] = useState(0); const onClickHandler = () => { console.log(`counter = ${counter}`); } return <Child onClick={onClickHandler} />; }
Here the onClickHandler()
function fails referential equality because a different function is created during every Parent render. This causes the Child to re-render even though onClickHandler
is doing the same thing. To avoid this, we use the useCallback()
hook.
const Parent = () => { const [counter, setCounter] = useState(0); const onClickHandler = useCallback(() => { console.log(`counter = ${counter}`); }, [counter]); return <Child onClick={onClickHandler} />; }
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useCallback(fn, deps)
is equivalent to useMemo(() => fn, deps)
.
Why useMemo
? Function-internal variables will get initialized during each execution. Thus it is important to keep them optimized. Costly computations should be cached. Memoization is a recommended pattern to achieve this.
function computeExpensiveValue(a, b) { // Let's say, this imaginary %|% operator is very expensive return a %|% b ; } const compute = useMemo(() => computeExpensiveValue(a, b), [a, b]); compute(1, 2); // will call computeExpensiveValue(1, 2) compute(1, 2); // no change in arguments. Will return cached value compute(2, 3); // arguments changed. Will call computeExpensiveValue(2, 3)
Remember that the function passed to useMemo
runs during rendering. Don’t do anything there that you wouldn’t normally do while rendering. For example, side effects belong in useEffect
, not useMemo
.
If no array is provided, a new value will be computed on every render.
Build your custom hook
Custom hooks allow you to extract stateful logic out of your component and reuse it between components.
Traditionally in React, we’ve had two popular ways to share stateful logic between components: render props and higher-order components. We will now look at how Hooks solve many of the same problems without forcing you to add more components to the tree.
You can write custom Hooks that cover a wide range of use cases like form handling, animation, declarative subscriptions, timers, and probably many more.
A custom Hook is a JavaScript function whose name starts with ”use
” and that may call other Hooks. The useSomething
naming convention is how the linter plugin is able to find bugs in the code using Hooks.
For example to see the online chat status of a friend:
import React, { useState, useEffect } from 'react'; function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); function handleStatusChange(status) { setIsOnline(status.isOnline); } useEffect(() => { ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); return isOnline; }
Your hook can now be used in other components:
function FriendStatus(props) { const isOnline = useFriendStatus(props.friend.id); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; }
function FriendListItem(props) { const isOnline = useFriendStatus(props.friend.id); return ( <li style={{ color: isOnline ? 'green' : 'black' }}> {props.friend.name} </li> ); }
The state of each component is completely independent. Hooks are a way to reuse stateful logic, not state itself. In fact, each call to a Hook has a completely isolated state — so you can even use the same custom Hook twice in one component.
No comments yet.