
Starting from version 16.8 ReactJS introduced a new, modern, way of developing React web apps using a feature they called Hooks. This article introduces Hooks and demonstrates how to create React components in this more modern style.
Hooks are functions that allow developers to hook-into React’s lifecycle and state from components declared as functions instead of classes. In fact, hooks don’t work in React classes at all.
A great feature of Hooks is that it can sit side-by-side with traditional React classes. Both techniques can be used to create React components and use them in your web app projects. Because of this existing apps can become a mixture of React class based components and Hooks based components without any issues, and you can decide if and when you want to update the older React classes into Hooks based components.
You can also create your own custom Hooks that can be used in your components too.
The first thing to look at is how we define components as functions in modern React. This is easy in its simplest form, for example:
export default function HelloWorld() {
return <div>Hello World!</div>;
}
Code language: TypeScript (typescript)
If you want to support initialisation properties then you can modify the signature like so (I’ve chosen to strongly type the init props – this isn’t mandatory):
export interface HelloUserProps {
name: string
}
export default function HelloUser(props: HelloUserProps) {
return <div>Hello {props.name}!</div>
}
Code language: JavaScript (javascript)
The above is fine as a basic use case but we didn’t use any Hooks. They are easy enough to learn and use, so here’s an example that uses the useState
Hook to make the HelloUser
component more dynamic.
export interface HelloUserProps {
name: string
}
export default function HelloUser(props: HelloUserProps) {
const nameTextBoxId: string = 'newNameTextBox';
const [ name, setName ] = useState(props.name);
const changeName = () => {
var textBox = document.getElementById(nameTextBoxId);
setName(textBox.value);
};
return <div>
<div>Hello {name}!</div>
<div>
<input id="{nameTextBoxId}" type="text" />
<button type="button" onClick={() => { changeName() }}>Change Name</button>
</div>
<div>;
}
Code language: PHP (php)
We’re just using useState
above, but notice we declare it using array destructuring. This is common practice with Hooks, as the methods often return the current value and one or more actions (e.g. to set a new value in the state variable in this case).
Something else to mention is that Hooks can only be used in certain places. This is typically at the start of a component, before any layout rendering. Hooks cannot be used outside components and React’s runtime is likely to raise software exceptions if this is attempted.
Here is a fairly comprehensive list of the available Hooks in React, and brief descriptions for them:
- useState. Allows you to register a state variable for your component, and interact with it. This is likely to be your most commonly used Hook.
- useEffect. Provides a means to synchronise a component with an external system, such as pulling data from a Web API, for example. Effects only run when you are using client-side rendering (CSR), so they won’t execute if you are using server-side rendering (SSR) in React.
- useContext. Contexts allow a parent component to make specific data available to all children in its rendering sub-tree without having to daisy chain the data via initialisation properties. This Hook allows a context to be consumed by the child. (Aside: the context itself is created by the parent using
createContext
, but this is a whole other topic we won’t go into any further here.) - useReducer. Lets you specify a reducer function that can be used to update a state variable. The Hook returns the current state and a dispatch function allowing you to update the state.
- useCallback. Allows you to cache a function on initial render of a component and then retrieve that cached function on subsequent renders. A new function instance will only be cached if the dependencies of the callback have changed.
- useMemo. Lets you cache a calculation performed by a function on initial render of a component and then retrieve that cached value on subsequent renders. A new calculation will only be performed and cached if the dependencies of the memo have changed.
- useRef. Allows you to access/reference a value that you don’t want to control rendering. The returned ref object exposes a
current
property that allows you to update it. You may think this is similar touseState
, except changes to the value of a ref don’t trigger component re-rendering. - useLayoutEffect. This Hook works similarly to
useEffect
, except it fires just before the browser repaints the screen. As such, you can use it to get layout coordinates/measurements. Effects only run client-side, so won’t execute if you are using SSR in React. Also, using this Hook may harm app performance so preferuseEffect
wherever possible. - useDebugValue. Allows you to add debugging labels that will appear in React DevTools to your custom Hook code.
- useDeferredValue. On initial render a deferred value will be the same as the value you provide, but on subsequent renders it will lag behind and only update the deferred value after rendering. It will essentially return you the value you supplied in the previous render until after the current render is complete.
- useTransition. Transitions allow you to perform actions in the background without blocking the UI, allowing components to re-render and remain responsive while waiting for those transitions to complete. For example, a parent component may contains a description, and a table and a chart that need populating from source data retrieved from a Web API. The component can start a transition to retrieve the data and then continue rendering, showing the description and a ‘loading data’ placeholder instead of the table and chart until the transition completes; and then the table and chart can be rendered with data.
- useId. Generates a unique ID in string format that can be used for ARIA accessibility attributes. For example, calling this Hook will return an ID that can be used as the
id
attribute of a HTML control describing an input field (for example) and that same ID can then be used as thearia-describedby
attribute value on the input field for use with screen readers, etc. - useSyncExternalStore. Allows your component to synchronise with an external store in scenarios where it cannot fully maintain its own state or receive data via initialisation properties, etc. For example, your app needs to persist state between sessions on the same browser, so you write code (JS) compatible with this Hook to save the persisted state to IndexedDB and read it back when the component needs it.
- useInsertionEffect. Supports runtime-injection of CSS
style
tags into the DOM to allow styling as JS in code. This is not recommended and should be avoided unless you are already using a CSS-in-JS library or similar tool, and specifically want your styling defined in (JS) code.
Let’s look at a simple example using some of the Hooks mentioned above.
import React from "react";
import { useState, useEffect, useId, useTransition } from "react";
import { Spinner } from '../shared/Spinner';
interface SalesRow {
id: number,
date: Date,
soldBy: string,
productCode: string,
productName: string,
quantity: number,
unitPrice: number,
totalValue: number
}
export default function TotalSalesReport() {
const [requiresRefresh, setRequiresRefresh] = useState(true);
const [salesData, setSalesData] = useState([] as Array<SalesRow>);
const [totalSales, setTotalSales] = useState(0);
const [isLoading, startTransition] = useTransition();
const refreshButtonDescriptionId = useId();
// This has a dependency on requiresRefresh so it runs on initial render and
// then conditionally, only when that state variable changes.
useEffect(
() => {
if (requiresRefresh) {
setRequiresRefresh(false);
startTransition(() => {
// TODO: retrieve sales data from back-end service.
// Code omitted for brevity in this example.
setSalesData([]);
setTotalSales(0);
});
}
},
[requiresRefresh]
);
return (
<div>
<h3>Total Sales (YTD)</h3>
{isLoading
? <Spinner message="Loading..." />
: <div>
<button type="button" aria-describedby={refreshButtonDescriptionId}
onClick={() => { setRequiresRefresh(true) }}>
Refresh
</button>
<span id={refreshButtonDescriptionId}>
Loads the latest data from the server.
</span>
<br />
<table className="table">
<thead>
<tr>
<th>Sales ID</th>
<th>Date</th>
<th>Sold By</th>
<th>Product Code</th>
<th>Product Name</th>
<th>Quantity</th>
<th>Unit Price</th>
<th>Total Value</th>
</tr>
</thead>
<tbody>
{
salesData.map(row => (
<tr>
<td>{row.id}</td>
<td>{row.date.toDateString()}</td>
<td>{row.soldBy}</td>
<td>{row.productCode}</td>
<td>{row.productName}</td>
<td>{row.quantity}</td>
<td>{row.unitPrice}</td>
<td>{row.totalValue}</td>
</tr>
))
}
</tbody>
</table>
<div className="sales-total">Total Sales: {totalSales}</div>
</div>
}
</div>
);
}
Code language: TypeScript (typescript)
Something to note in the code immediately above is the way we’re using the useEffect
Hook. If you call it with no dependencies defined, it will run on each render. But, if you call it with one or more dependencies then it will only run on the intial render and when any of those dependencies have changed. This is also the case for some of the other Hooks so bear that in mind when developing your components to ensure correct behaviour.