Get Hooked on React!

Get Hooked on React!

By Mark Volkmann, OCI Partner and Principal Software Engineer

December 2018

Overview

Hooks are a feature added in React 16.7.0-alpha.0. They enable implementing stateful components with functions instead of classes.

Hooks are expected to be ready for production use in React 16.7, which should be released in Q1 2019.

There are no plans to remove existing ways of implementing components. Hooks simply provide a new way that most developers will find easier to understand and are backward-compatible with existing code.

Components that use hooks can be used together with class-based components. Existing apps can choose to gradually incorporate hooks or never use them.

Eventually it will be possible to use function components to do everything that is currently possible with class components. However, there are some lifecycle methods (componentDidCatch and getSnapshotBeforeUpdate) whose functionality cannot yet be implemented using hooks.

Hooks are currently considered experimental, and the API may still change.

Benefits of Hooks

Hooks support implementing nearly any component without creating a class. Many developers find classes confusing because they need to understand the this keyword, what it means to bind a function, and when to do it.

Optimizing code that uses functions is easier than optimizing code that uses classes. This refers to minifying, hot reloading, and tree shaking.

Hooks provide easier ways to work with component state and context.

Hooks make it easier to reuse state logic between multiple components. In most cases this removes the need for higher-order components and render props, both of which require increased levels of code nesting.

Hooks support using "effects" in place of lifecycle methods. This makes it possible to better organize related code, such as adding/removing event listeners and opening/closing resources.

Rules For Hooks

Hook function names should start with "use." This convention allows linting rules to check for proper use of hooks and provides a clue that the function may access state.

Hook functions should only be called in function-based components and in custom hooks.

Hook functions should not be called conditionally. This means they should not be called in if/else blocks, loops, or nested functions. This ensures that for any component, the same hooks are invoked in the same order on every render.

React provides ESLint rules to detect violations of these rules. See https://www.npmjs.com/package/eslint-plugin-react-hooks. This currently implements a single rule named "react-hooks/rules-of-hooks" that should be configured with a value of "error." This rule assumes that any function whose name begins with "use" followed by an uppercase letter is a hook. It verifies that hooks are only called from function components (name starts uppercase) or custom hook functions. It also verifies that hooks will be called in the same order on every render.

A future version of create-react-app will automatically configure the react-hooks/rules-of-hooks ESLint rule.

Provided Hooks

The hooks provided by React are implemented as functions that are exported by the react package.

Each of the provided hooks is described in the following sections. They are somewhat ordered based on how frequently they are expected to be used.

State Hook

The useState function is a hook that provides a way to add state to function components. It takes the initial value of the state and returns an array containing the current value and a function to change it. This allows a component to use state without using the this keyword.

For example, the following code can appear inside a function that defines a component.

const [petName, setPetName] = useState('Dasher');
const [petBreed, setPetBreed] = useState('Whippet');

In this example, petName holds the current value of the state. setPetName is a function that can be called to change the value. These "set" functions can be passed a new value or a function that will be passed the current value and return the new value. Calls to them trigger the component to be re-rendered.

Often the state is a primitive value, but it can also be an object.

If a state value is an object, calls to the corresponding set function must pass an entire new value. The set functions do not merge the object passed to them with the current value as is done by the Component setState method, which merges the top-level properties.

Note that the useState calls are made every time the component is rendered. This allows the component to obtain the current value for each piece of state. Initial values are only applied during the first render.

Here is a complete component definition that uses the state hook:

  1. import React, {useState} from 'react';
  2.  
  3. export default function Pet() {
  4. const [petName, setPetName] = useState('Dasher');
  5. const [petBreed, setPetBreed] = useState('Whippet');
  6. const changeBreed = e => setPetBreed(e.target.value);
  7. const changeName = e => setPetName(e.target.value);
  8. return (
  9. <div>
  10. <label htmlFor="name">
  11. Name
  12. <input id="name" onChange={changeName} value={petName} />
  13. </label>
  14. <br />
  15. <label htmlFor="breed">
  16. Breed
  17. <select
  18. id="breed"
  19. onBlur={changeBreed}
  20. onChange={changeBreed}
  21. value={petBreed}
  22. >
  23. <option>Greyhound</option>
  24. <option>Italian Greyhound</option>
  25. <option>Whippet</option>
  26. </select>
  27. </label>
  28. <div>
  29. {petName} is a {petBreed}.
  30. </div>
  31. </div>
  32. );
  33. }

It's not necessary to understand how this works, but it is interesting.

The state values for a component are stored in a linked list. Each call to useState associates a state value with a different node in the linked list. In the example above, petName is stored in the first node and petBreed is stored in the second node.

The useState hook adds state to the component that uses it. The custom hook useTopState adds state outside of components that can be shared with any number of components. If any component changes the state, all the components that use it are re-rendered. See https://www.npmjs.com/package/top-state-hook.

Effect Hook

The useEffect hook provides an alternative to lifecycle methods like componentDidMountcomponentDidUpdate, and componentWillUnmount in function components.

Effects have two phases: setup and cleanup.

Think of setup as being performed when a class component would call componentDidMount or componentDidUpdate, which is after React updates the DOM. Examples of setup functionality include fetching data (e.g., calling a REST service), registering an event listener, opening a network connection, and starting a timeout or interval.

Think of cleanup as being performed when a class component would call componentWillUnmount. Examples of cleanup functionality include unregistering an event listener, closing a network connection, and clearing a timeout or interval.

An effect is configured by calling the useEffect function, which takes a function that performs the setup. If no cleanup is needed, this function returns nothing. If cleanup is needed, this function returns another function that performs the cleanup.

For example:

useEffect(() => {
  console.log('performing setup');
  return () => {
    console.log('performing cleanup');
  };
});

The useEffect function can be called any number of times inside a function component. It is typically called once for each distinct kind of effect, rather than combining the code for multiple effects in a single call.

In the first render of a component, the order of execution is:

  1. all the code in the component function
  2. the setup code in all the effects in the order defined

In subsequent renders, the order of execution is:

  1. all the code in the component function
  2. the cleanup code in all the effects in the order defined (not reverse order)
  3. the setup code in all the effects in the order defined

If it is desirable to prevent the setup and cleanup code from running in every render, supply a second argument to the useEffect function that is an array of variables. The cleanup and setup steps are only executed again if any of these variables have changed since the last call.

One use of an effect is to move focus to a particular input. This is demonstrated in the "Ref Hook" section later.

Context Hook

The useContext hook provides an alternative way to consume context state in function components.

Hooks do not change the way context providers are implemented. They are still implemented by creating a class that extends from React.Component and renders a Provider. For details, see https://reactjs.org/docs/context.html.

Suppose a context provider has been implemented in the component SomeContext. The useContext hook can be used in another component to access its state.

For example:

import {SomeContext} from './some-context';
 
export default MyComponent() {
  const context = useContext(SomeContext);
  return <div>{context.someData}</div>;
}

The context variable is set to an object that provides read-only access to the state properties of the context and access to any methods defined on it. These methods can provide a way for context consumers to modify the context state.

Directly setting properties on the context variable affects the local object, but not the context state. Doing this is not flagged as an error.

Calling the useContext function also subscribes the function component to context state updates. Whenever the context state changes, the function component is re-rendered.

To avoid re-rendering the component on every context state change, wrap the returned JSX in a call to useCallback described next.

A great use of the useContext hook is in conjunction with the npm package "context-easy". See https://www.npmjs.com/package/context-easy.

Callback Hook

The useCallback hook takes an expression and an array of variables that affect the result. It returns a memoized value. Often the expression is a function.

This can be used to avoid recreating callback functions defined in function components every time they are rendered. Such functions are often used as DOM event handlers.

For example, consider the difference between these:

<input onChange={e => processInput(e, color, size)}>
<input onChange={useCallback(e => processInput(e, color, size), [color, size]}>

These lines have the same functionality, but the second line only creates a new function for the onChange prop when the value of color or size has changed since the last render.

Avoiding the creation of new callback functions allows the React reconciliation process to correctly determine whether the component needs to be re-rendered. Avoiding unnecessary renders provides a performance benefit.

If the callback function does not depend on any variables, pass an empty array for the second argument. This causes useCallback to always return the same function. If the second argument is omitted, a new function will be returned on every call which defeats the purpose.

The useCallback hook can also serve as a substitute for the lifecycle method shouldComponentUpdate available in class components.

For example, suppose v1 and v2 are variables whose values come from calls to useState or useContext, and these are used in the calculation of JSX to be rendered. To only calculate new JSX if one or both of them have changed since the last render, pass the JSX as the first argument to useCallback.

For example:

return useCallback(<div>component JSX goes here.</div>, [v1, v2]);

Memo Hook

The useMemo hook takes a function and an array of variables that affect the result. It memoizes the function and returns its current result.

For example, suppose x and y are variables whose values come from calls to useState or useContext, and we need to compute a value based on these. The following code reuses the previous result if the values of x and y have not changed.

const hypot = useMemo(
  () => {
    console.log('calculating hypotenuse');
    return Math.sqrt(x * x + y * y);
  },
  [x, y]
);

This only remembers the result for the last set of input values. It does not store all past unique calculations.

Note the difference between useCallback and useMemo. While both provide memoization, useCallback returns a value (which could be a function), and useMemo returns the result of calling a function.

React.memo function

The React.memo function, not a hook, was added in React 16.6. It memoizes a function component so it is only re-rendered if at least one of its props has changed. This does what class components do when they extend from PureComponent instead of Component.

For example, the following code defines a Percent component that renders the percentage a count represents of a total.

import React from 'react';
 
export default React.memo(({count, total}) => {
  console.log('Percent rendering'); // to verify when this happens
  return <span>{((count / total) * 100).toFixed(2)}%</span>;
});

Reducer Hook

The useReducer hook supports implementing components whose state is updated by dispatching actions that are handled by a reducer function. It is patterned after Redux. It takes a reducer function and the initial state.

Here's an example of a very simple todo app that uses this hook. It uses Sass for styling. Note how the TodoList component calls useReducer to obtain the state and the dispatch function. It calls the dispatch function in its event handling functions.

ToDo List App screenshot

todo-list.scss

  1. .todo-list {
  2. .delete-btn {
  3. background-color: transparent;
  4. border: none;
  5. color: red;
  6. font-weight: bold;
  7. }
  8.  
  9. .done-true {
  10. color: gray;
  11. text-decoration: line-through;
  12. }
  13.  
  14. form {
  15. margin-bottom: 10px;
  16. }
  17.  
  18. .todo {
  19. margin-bottom: 0;
  20. }
  21. }

todo-list.js

  1. import React, {useReducer} from 'react';
  2. import './todo-list.scss';
  3.  
  4. const initialState = {
  5. text: '',
  6. todos: []
  7. // objects in this have id, text, and done properties.
  8. };
  9.  
  10. let lastId = 0;
  11.  
  12. function reducer(state, action) {
  13. const {text, todos} = state;
  14. const {payload, type} = action;
  15. switch (type) {
  16. case 'add-todo': {
  17. const newTodos = todos.concat({id: ++lastId, text, done: false});
  18. return {...state, text: '', todos: newTodos};
  19. }
  20. case 'change-text':
  21. return {...state, text: payload};
  22. case 'delete-todo': {
  23. const id = payload;
  24. const newTodos = todos.filter(todo => todo.id !== id);
  25. return {...state, todos: newTodos};
  26. }
  27. case 'toggle-done': {
  28. const id = payload;
  29. const newTodos = todos.map(todo =>
  30. todo.id === id ? {...todo, done: !todo.done} : todo
  31. );
  32. return {...state, todos: newTodos};
  33. }
  34. default:
  35. return state;
  36. }
  37. }
  38.  
  39. export default function TodoList() {
  40. const [state, dispatch] = useReducer(reducer, initialState);
  41.  
  42. const handleAdd = useCallback(() => dispatch({type: 'add-todo'}), []);
  43. const handleDelete = useCallback(
  44. id => dispatch({type: 'delete-todo', payload: id}),
  45. []
  46. );
  47. const handleSubmit = useCallback(e => e.preventDefault(), []); // prevents form submit
  48. const handleText = useCallback(
  49. e => dispatch({type: 'change-text', payload: e.target.value}),
  50. []
  51. );
  52. const handleToggleDone = useCallback(
  53. id => dispatch({type: 'toggle-done', payload: id}),
  54. []
  55. );
  56.  
  57. return (
  58. <div className="todo-list">
  59. <h2>Todos</h2>
  60. <form onSubmit={handleSubmit}>
  61. <label htmlFor="text">
  62. <input
  63. placeholder="todo text"
  64. onChange={handleText}
  65. value={state.text}
  66. />
  67. </label>
  68. <button onClick={handleAdd}>+</button>
  69. </form>
  70. {state.todos.map(todo => (
  71. <div className="todo" key={todo.id}>
  72. <input
  73. type="checkbox"
  74. onChange={() => handleToggleDone(todo.id)}
  75. value={todo.done}
  76. />
  77. <span className={`done-${todo.done}`}>{todo.text}</span>
  78. <button className="delete-btn" onClick={() => handleDelete(todo.id)}>
  79. X
  80. </button>
  81. </div>
  82. ))}
  83. </div>
  84. );

Ref Hook

The useRef hook provides an alternative to using class component instance variables in function components. Refs persist across renders. They differ from capturing data using the useState hook in that changes to their values do not trigger the component to re-render.

The useRef function takes the initial value and returns an object whose current property holds the current value.

A common use is to capture references to DOM nodes.

For example, in the Todo app above, we can automatically move focus to the text input. To do this we need to:

  1. import the useEffect and useRef hooks
  2. create a ref inside the function
  3. add an effect to move the focus
  4. set the ref using the input element ref prop
const inputRef = useRef();
useEffect(() => inputRef.current.focus());
 
return (
  ...
  <input
    placeholder="todo text"
    onChange={handleText}
    ref={inputRef}
    value={state.text}
  />;
  ...
)

Ref values are not required to be DOM nodes.

For example, suppose we wanted to log the number of todos that have been deleted every time one is deleted. To do this we need to:

  1. create a ref inside the function that holds a number
  2. increment the ref value every time a todo is deleted
  3. log the current value
const deleteCountRef = useRef(0); // initial value is zero
 
// Modified version of the handleDelete function above.
const handleDelete = id => {
  deleteCountRef.current++;
  dispatch({type: 'delete-todo', payload: id});
  console.log('You have now deleted', deleteCountRef.current, 'todos.');
};

Imperative Methods Hook

The useImperativeMethods hook modifies the instance value that parent components will see if they obtain a ref to the current component.

One use is to add methods to the instance that parent components can call.

For example, suppose the current component contains multiple inputs. It could use this hook to add a method to its instance value that parent components can call to move focus to a specific input.

Layout Effect Hook

The useLayoutEffect hook is used to query and modify the DOM. It is similar to useEffect but differs in that the function passed to it is invoked after every DOM mutation in the component. DOM modifications are applied synchronously.

One use for this is to implement animations.

Mutation Effect Hook

The useMutationEffect hook is used to modify the DOM. It is similar to useEffect but differs in that the function passed to it is invoked before any sibling components are updated. DOM modifications are applied synchronously.

If it is necessary to also query the DOM, the useLayoutEffect hook should be used instead.

Custom Hooks

A custom hook is a function whose name begins with "use" and calls one more hook functions. They typically return an array or object that contains state data and functions that can be called to modify the state.

Custom hooks are useful for extracting hook functionality from a function component so it can be reused in multiple function components.

For example, Dan Abramov demonstrated a custom hook that watches the browser window width. (See the video link in references.)

function useWindowWidth() {
  // This maintains the width state for any component that calls this function.
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    // setup steps
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => {
      // cleanup steps
      window.removeEventListener('resize', handleResize);
    };
  }, []);
}

To use this in a function component:

const width = useWindowWidth();

Another example Dan Abramov shared simplifies associating a state value with a form input. It assumes the state does not need to be maintained in an ancestor component. (See the video link in references.)

function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);
  const onChange = e => setValue(e.target.value);
  // Returning these values in an object instead of an array
  // allows it to be spread into the props of an HTML input.
  return {onChange, value};
}

To use this in a function component that renders an input element for entering a name:

const nameProps = useFormInput('');
 
return (
  <input {...nameProps} />;
);

Third Party Hooks

The React community is busy creating and sharing additional hooks. Many of these are listed at https://nikgraf.github.io/react-hooks/.

Conclusion

Hooks are a great addition to React! They make implementing components much easier. They also likely spell the end of implementing React components with classes. However, you may not want to use them in production apps just yet, since they are still considered experimental and their API may change.

Thanks to Jason Schindler for reviewing this article!

Resources

Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.