blog post

Mastering React Hooks: A Comprehensive Guide to Functional Components

Introduction

The introduction of Hooks in React 16.8 significantly changed the coding style for building React components. Hooks allow to control component's state, lifecycle and offer a more powerful and flexible way to create and manage functional components.

Prior to Hooks, stateful components required the use of classes, which could complicate the codebase and increase a learning curve for new frontend developers. Hooks provide a way to "hook into" React state and lifecycle features from function components. In this blog post you will learn the essentials and the most common React Hooks.

useState hook

useState allows to save and update data inside the functional component that persists through multiple component re-renders.

tsx
import { useState } from "react"; export const CoffeeCounter = () => { const [count, setCount] = useState(0); return ( <div> <p>You have {count} coffee(s)</p> <button onClick={() => setCount(count + 1)}> + 1 coffee </button> <button onClick={() => setCount(count - 1)}> - 1 coffee </button> </div> ); }

Here we define a count variable inside a CoffeeCounter component. The useState hook contains a variable itself and a function that allows to a change value of the variable. Everytime a setCount function is called - a component is rendered with a new value of count variable.

In this example we created a simple counting component that increases and decreases count by 1 every time you press a corresponding button.

In some use cases you might not have the actual value of count variable, for example in callbacks. In this case you can use another form of setCount function that has a previous value of the variable:

tsx
<button onClick={() => setCount(prev => prev + 1)}> Click Me </button>

In the useState hook you can store any types: primitive variables, arrays, objects. Let's have a look on how to store objects in the component's state:

tsx
export const PersonData = () => { const [person, setPerson] = useState<Person>({ name: '', age: 0 }); const savePerson = () => { setPerson({ name: "Anton", age: 20 }); }; return ( <div> <div>{person.name} ({person.age} years)</div> <button onClick={() => savePerson()}> Set person data </button> </div> ); };

useEffect hook

useEffect hook allows to hook into component's life cycle and add side effects such as API calls, calculations, manual DOM updates, etc.

tsx
import { useEffect, useState } from "react"; export const CoffeeCounter = () => { const [count, setCount] = useState(0); useEffect(() => { console.log("Page loaded"); }, []); useEffect(() => { console.log("Count updated: ", count); }, [count]); return ( // Rendering code remains the same ); }

useEffect hooks has 2 parameters: a function that performs a side effect and an array of tracked variables. When a variable value changes - a useEffect hook is triggered. The first hook has an empty braces [ ] without parameters, it means that this effect will be called only once after a component is loaded. This is a perfect place to load some data from the API when page is loaded. Second hook is called only when count variable is changed. In the CoffeeCounter component logs a message to console when a page is loaded and after clicking any button that changes the count value.

useReducer hook

useReducer is helpful when you need to perform complex state updates. Let's rewrite our CoffeeCounter component and replace useState hook with useReducer:

tsx
import {useReducer} from "react"; type CounterState = { count: number }; type CounterAction = { type: string }; function reducer(state: CounterState, action: CounterAction) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: throw new Error(); } } export const ReducerCounter = () => { const [state, dispatch] = useReducer(reducer, { count: 0 }); return ( <div> <p>You have {state.count} coffee(s)</p> <button onClick={() => dispatch({ type: 'increment' })}> + 1 coffee </button> <button onClick={() => dispatch({ type: 'decrement' })}> - 1 coffee </button> </div> ); }

Instead of just calling a set function that sets a new value, a dispatch method from useReducer is called that can perform a complex state manipulations.

Every state update using a dispatch function requires a name of the action type, in our example we have 2 actions: increment and decrement. After dispatch function is executed, a reducer function comes into play and performs state updates depending on a given action.

useCallback and useMemo

useCallback and useMemo help in optimizing performance by memoizing functions and values respectively, avoiding unnecessary re-renders.

tsx
import React, {useCallback, useMemo, useState} from "react"; type ExpensiveComponentProps = { compute: (n: number) => number; count: number; } const ExpensiveComponent: React.FC<ExpensiveComponentProps> = ({ compute, count }) => { const result = useMemo(() => compute(count), [compute, count]); return <div>Result: {result}</div>; }; const ParentComponent: React.FC = () => { const [count, setCount] = useState<number>(0); const increment = useCallback(() => { setCount(c => c + 1); }, []); return ( <div> <ExpensiveComponent compute={(n: number) => n * 2} count={count} /> <button onClick={increment}>Increment</button> </div> ); }; export { ExpensiveComponent, ParentComponent };

ExpensiveComponent accepts two props: compute function and a count number. The component uses the useMemo React hook to memorize the result of calling the compute() function with the count argument. This result will only be re-calculated if the compute function or the count prop changes.

useCallback is used to prevent unnecessary re-renders of components. Without useCallback, function props passed down to child components can cause them to re-render whenever the parent component renders, even if the passed functions didn't change logically. This is because a new function is created on each render and JavaScript can't perform function equality checks.

In this example, the count state variable in the ParentComponent is incremented by 1 each time the button is clicked. When the ExpensiveComponent receives a prop function compute - it doubles the input. So each time Expensive component receives a counter variable - it doubles the result. On a screen you will see the following results: 0, 2, 4, 6, 8 (doubled from 0, 1, 2, 3, 4 respectively) and so on.

Summary

React Hooks is a modern way you build components in React, offering a more intuitive and functional approach to state management and side effects. By mastering Hooks, you can simplify your components, make your code more readable and maintainable. In this blog post we explored the most common React Hooks.

  • The useState hook allows data to be saved and updated within functional components that persist through multiple component renders.
  • The useEffect hook adds side effects such as API calls and calculations, to the component's lifecycle.
  • The useReducer hook provides a more flexible method to manage complex state logic in a component, acting similar to Redux’s reducer for local state management.
  • The useCallback hook memorizes a callback function and only changes when its dependencies do, hence preventing unnecessary re-renders in child components expecting this callback as a prop.
  • The useMemo hook is used to memorize expensive computations and only recompute them when necessary, optimizing performance by avoiding unnecessary calculations.

Hope you find this blog post useful. Happy coding!

Improve Your .NET and Architecture Skills

Join my community of 2300+ developers and architects.

Each week you will get 1 practical tip with best practises and architecture advice.