React hooks

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 state
  • useRef: a hook to access a component’s DOM element
  • useEffect: 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:

Possible solution 1: You could each let them fetch the data separately. That works, but it comes at the cost of having two requests and the changing the sate in one component makes it out of sync with the other.

Possible solution 2: You lift state up the component tree, meaning that the common state is handles 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.

Solution with useContext: 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

Caches a function, such as callback functions that you use in child components. If the parent functional component uses regular functions to do this, it sends a different function reference each time. This prompts the child to re-render more than it needs to.

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 do re-render even though onClickHandler is the pretty much the same function. 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} />;
}

useCallBack() makes sure that the function passed down to component only changes if its dependency (counter) change.

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.

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); // call computeExpensiveValue(1, 2)
compute(1, 2); // no change in arguments. Return cached value
compute(2, 3); // call computeExpensiveValue(2, 3)

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.

About Author

Mathias Bothe Contact me

I am Mathias, born 38 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 14 years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I am founder of bosy.com, creator of the security service platform BosyProtect© and initiator of several other software projects.

No comments yet.

Leave a comment