Skip to main content

React Hooks

The most popular React Hooks are outlined here.

useState

React provides the useState hook so that we can manage our state and declare which change in information should re-render the component.

Usage

The useState function takes an argument, the initial value, and returns an array. By using array destructuring we can obtain the state as first and the updater function as second argument like that:

const [state, setState] = useState(<initial value>);

// with this state update you will loose the dog object
setState(...);

We can e.g. use the first argument for value binding and the updater function to update the state from within an event-handler.

Best Practise

By following the best practices below you should be fine.

Never use useState directly in the JSX you return

State manipulation should result from user interaction (event-handler). When you manipulate the state in your template, then you trigger a re-render cycle. Therefore, only manipulate state from inside your event-handler.

Use useState per independent unit you have to manage

As you can use the useState-Hook multiple times in your component, you can use it per unit you need to manage. If you don't do that you might accidentally loose state when you perform an update. Example:

const [value, setValue] = useState({ 
dog: { name: 'Rex' },
owner: { name: 'Frank' }
});

// with this state update you will loose the dog object
setValue({ owner: { name: 'Hans' } });

A better way is to split the state into two units as follows:

const [dog, setDog] = useState({ dog: { name: 'Rex' } });
const [owner, setOwner] = useState({ owner: { name: 'Frank' } });

setOwner({ owner: { name: 'Hans' } });

Note that this is a different behaviour than with the class component and the state-property, where the other properties get preserved.

Of course, you could also manage the whole state with one object by copying the previous state as such:

const [value, setValue] = useState({ 
dog: { name: 'Rex' },
owner: { name: 'Frank' }
});

// with this state update we first copy the previous state and then overwrite it with the new change
setValue({ ...value, owner: { name: 'Hans' } });

Use () => when using a function to compute the initial state

When you need to compute the initial state and you want to do it lazily - the first time the component renders, then use a function call like that:

const expensiveInitialStateCalculationFn = () => { {/* ... */} }
setOwner(() => expensiveInitialStateCalculationFn());

// don't do: setOwner(expensiveInitialStateCalculationFn()); as it will execute immediately

When you need the latest state then pass an argument along the setState function

If your new state is based on the previous old state then you must pass an argument to the updater function. React makes sure that the value will be the previous value.

setOwner(prevOwner => { owner: { name: `${prevOwner} & Hans` } });

useEffect

React provides the useEffect hook so that we can better control side-effects and integrate them into the component lifecycle.

The useEffect hook is really powerful and can be used in many scenarios, such as:

  • Running code after every render
  • Running code only once when a component mounts
  • Running clean up code by returning a function

Usage

The useEffect hook takes two arguments, a function which is the effect itself and an optional array of dependencies which specifies when the effect gets executed.

useEffect(() => { 
// runs only once when a component mounts
}, [/* dependent variables */]);

Pitfalls

When you use dependencies inside useEffect that you did not declare in the array (2nd param), then you might face issues with stale references. For example you get a callback passed as prop. When you don't declare it and the reference changes (callback method changes), then you code inside of the effect will call the old reference.

For more information read the note at the bottom of this page.

Scenarios

You can use the useEffect hook in the following ways.

Run an effect after every render cycle

Run an effect after every render cycle by specifying no dependencies:

useEffect(() => { 
// runs after each render cycle
});

Run an effect only once when the component gets mounted

React determines whether to run an effect by checking if the values in the array have changed since the last time the component called the effect. By providing an empty array, the content will never change, hence the effect runs only once when the component first mounts.

Run an effect only once when the component gets mounted by specifying an empty array as dependencies:

useEffect(() => { 
// runs only once when a component mounts, e.g. to perform an http-call to initialize the component
}, []);

Run cleanup code

Run cleanup code by returning a function inside the effect:

useEffect(() => { 
return () => { {/* your cleanup code, e.g. removing an event listener */} }
});

Best Practise

Fetching data and initializing the component

The best way to fetch data is to put an async function, which carries out the http-call, into the effect and to execute it afterwards. As we pass an empty dependency array the http-call gets executed only once.

const url = 'https://some-rest.api/dogs';
const [state, setState] = useState();

useEffect(() => {
async function fetchData() {
const response = await fetch(url);
const data = await response.json();
setState(data);
}
fetchData();
}, []);

Put separate side effects into separate calls to useEffect

This is similar to the rule we had before with the useState hook. By splitting up separate effects into separate function call it is easier to understand what each effect does. Also by splitting up the dependencies into separate effects it is easier to control when an effect runs.

useContext

React provides the useContext hook so that distant components can consume distant state directly, so that there is no need to pass this state through multiple layers of components. The Context API defines a state Provider-Component that provides a value via a context-object and the useContext hook for the consumer side to get the latest value from a certain context.

This API is very useful when you want to share rarely changing values used by many components (across the app).

Usage

The useContext hook takes one argument, the context, and returns the current value of it. const value = useContext(MyContext);

The context-object must be created with createContext, e.g.

import {createContext} from 'react';

const MyContext = createContext();

export default MyContext;

The current value is provided by the nearest <MyContext.Provider value={someValue}> above the calling component in the tree. The component calling useContext will re-render if the value changes.

Best Practise

If you can avoid the Context API do so

The Context API can easily be abused and an overuse destroys reusability of components. There are things you can do to avoid the Context API, see this link.

Create a Custom Provider to avoid re-renderings

To avoid unnecessary re-renderings it is better to extract the context logic into a custom provider like so:

import React, {createContext, useState} from "react"; 

const MyContext = createContext();
export default MyContext;

export function MyContextProvider ({children}) {
const [value, setValue] = useState(null);
return (
<MyContext.Provider value={{value, setValue}}>
{children}
</MyContext.Provider>
);
}

The consumer uses this provides in the following way:

import MyContextProvider from '...';
{/* ... */}
return (
<MyContextProvider>
...
</MyContextProvider>
);

We are using the children prop here, just as we would with a higher order component. Now when MyContextProvider re-renders, only the context consumer re-render and not the whole tree! The reason for this is that the children prop does not change, a state update won't change the props and therefore there is no need to re-render the children.

Context consumers however always re-render when the value of the closest provider for their context changes.

Keep your context as small as possible

Keep in mind that every time the context changes, all components that consume it have to rerender. Now, if you have just one context object with different kinds of values inside, values that don't belong together, a change of a value would cause all consumer to rerender.

So it is better to split up the big context and work with multiple smaller ones. You can nest as many contexts as you like, e.g.:

<ThemeContext.Provider value={theme}>
<UserContext.Provider value={user}>
...
</UserContext.Provider>
</ThemeContext.Provider>

Put providers as close as possible to the consumers

Again to avoid unnecessary re-renderings.

useReducer

React provides the useReducer hook so that we can better manage multiple state updates that have to happen together, in one shot. A practical example is an http-request which can be in progress, erroneous or successful. Depending in what state the request is, you have to set the state-properties accordingly.

Usage

The useReducer hook takes three arguments, and returns the current state and a dispatch function. The first argument is the reducer function, which is of type (state, action) => newState.

const [state, dispatch] = useReducer(reducerFn, initialState, init);

A simple example demonstrates the how a typical reducer function looks like:

function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1}; // in case of a bigger state you would return { ...state, <the actual update> }
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}

The reducer function gets triggered via the dispatch function, e.g. dispatch({ type: 'increment' });

The initial state can be initialized in two ways. Either with the second argument:

const [state, dispatch] = useReducer(
reducer,
{count: initialCount}
);

Or lazily via a function as a third argument, which will compute the initial state based on the second argument:

const init = (initialCount) => {count: initialCount};

const [state, dispatch] = useReducer(reducer, initialCount, init);

Best Practise

The reducer function

You can either use if else for a few cases or swtich case for more types of actions. In the default statement you should either return the state at hand or an error, e.g. return new Error('Unknown action type: ' + action.type)

useMemo

React provides the useMemo hook so that we can better control when to run expensive operations. E.g. we have an expensive function that would get triggered with each re-rendering, which would cause poor UX. To avoid that we can tell react to only call this function when a certain variable changes.

Usage

The useMemo hook takes a function as the first argument, dependent variables, that are required by that function, as second argument and returns the computed value. As the function is part of the render-cycle it must not cause side-effects. const computeExpensively = useMemo(() => someExpensiveComputation(a, b), [a, b]);

Now, on each call useMemo compares the input at hand with the previous input and if there is no change then it returns the already computed value, otherwise it will invoke the expensive function.

Best Practise

Always specify dependencies properly

When it comes to the second argument, there are two things you should avoid:

  • By omitting the second argument (dependent variables) the function will always get executed, so that would defeat its purpose.
  • By specifying an empty array as the second argument it may or may not use the stored value, because React may delete the memoized value if it has to free some memory. So also avoid that as well.

useCallback

React provides the useCallback hook for performance reasons. You can memoize a function which gets only re-executed when the specified params change. Otherwise the already computed value will get used instead.

Usage

The useCallback hook takes two arguments, the function to memoize, and its dependencies. Ideally the list of dependencies is equal to the params of the function.

const memoizedCallback = useCallback(
() => {
someExpensiveMethod(a, b);
},
[a, b],
);

useRef

React provides the useRef hook so that we can change the state that is not reflected to the user in the UI without triggering re-rendering.

These "internal values", which again are completely hidden from the user, can be timer- or element-IDs. You can for example set focus on elements in response to an event or read values of uncontrolled controls.

Usage

The useRef hook takes one argument, the initial value, and returns a ref object. const refObject = useRef(initialValue);

The refObject contains a current property, which contains the current value. Assigning new values to the current property of the ref object doesn't trigger a re-render, meaning we can maintain state across renderings.

Best Practise

Managing timers

You can manage timers by storing their ID in a ref (see example below).

const timerRef = useRef(null);

useEffect(() => {
timerRef.current = setInterval(() => {/* ... */} , 1000);
return clearInterval(timerRef.current);
}, []);

Managing controls

const ctrlRef = useRef();

// retrieve the value: ctrlRef.current.value
// set focus: ctrlRef.current.focus()

return (
<input
type="text"
ref={ctrlRef}
/>
);