b
Custom hooks
Hooks
React offers 15 different built-in hooks,
of which the most popular ones are the useState
and useEffect
.
We have already used both hooks extensively.
In part 5 we used the
useImperativeHandle
hook
which allows components to provide their functions to other components.
In part 6 we used
useReducer
and useContext
to implement a Redux-like state management.
Within the last couple of years, many React libraries have begun to offer hook-based APIs.
In part 6 we used the useSelector
and useDispatch
hooks from the react-redux library to share our redux-store and dispatch function to our components.
The React Router's API that we introduced in the
previous part is also partially hook-based.
Its hooks can be used to access URL parameters and the navigation
object, which allows for manipulating the browser URL programmatically.
As mentioned in part 1, hooks are not normal functions, and when using those we have to adhere to certain rules or limitations. Let's recap the rules of using hooks, copied verbatim from the official React documentation:
Donβt call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. You can only call Hooks while React is rendering a function component.
- β Call them at the top level in the body of a function component.
- β Call them at the top level in the body of a custom Hook ...
Itβs not supported to call Hooks (functions starting with use) in any other cases, for example:
- π΄ Do not call Hooks inside conditions or loops.
- π΄ Do not call Hooks after a conditional return statement.
- π΄ Do not call Hooks in event handlers.
- π΄ Do not call Hooks in class components.
- π΄ Do not call Hooks inside functions passed to useMemo, useReducer, or useEffect.
There's an existing ESlint rule that can be used to verify that the application uses hooks correctly.
To use this rule with vite, make sure you install the rule and plugin in your project.
npm i -D eslint eslint-plugin-react-hooks
You'll also want to add this rule to your package.json
{
//...
"eslintConfig": {
"extends": "plugin:react-hooks/recommended"
}
}
Once the rule is configured, the react-hooks rule will complain if hooks are used incorrectly.

If you do not see such an error here, then make sure that you have turned on Automatic ESLint configuration in File->New Projects Setup->Settings for New Projects.
Custom hooks
React offers the option to create custom hooks. According to React:
Building your own Hooks lets you extract component logic into reusable functions.
Custom hooks are regular JavaScript functions that can use any other hooks,
as long as they adhere to the rules of hooks.
Additionally, the name of custom hooks must start with the word use
.
We implemented a counter application in part 1 that can have its value incremented, decremented, or reset. The code of the application is as follows:
import { useState } from 'react'
const App = () => {
const [counter, setCounter] = useState(0)
return (
<div>
<div>{counter}</div>
<button onClick={() => setCounter(counter + 1)}>
plus
</button>
<button onClick={() => setCounter(counter - 1)}>
minus
</button>
<button onClick={() => setCounter(0)}>
zero
</button>
</div>
)
}
Let's extract the counter logic into a custom hook. The code for the hook is as follows:
const useCounter = () => {
const [value, setValue] = useState(0)
const increase = () => {
setValue(value + 1)
}
const decrease = () => {
setValue(value - 1)
}
const zero = () => {
setValue(0)
}
return {
value,
increase,
decrease,
zero
}
}
Our custom hook uses the useState
hook internally to create its state.
The hook returns an object, the properties of which include the value of the counter as well as functions for manipulating the value.
React components can use the hook as shown below:
const App = (props) => {
const counter = useCounter()
return (
<div>
<div>{counter.value}</div> <button onClick={counter.increase}> plus
</button>
<button onClick={counter.decrease}> minus
</button>
<button onClick={counter.zero}> zero
</button>
</div>
)
}
By doing this we can extract the state of the App
component and its manipulation entirely into the useCounter
hook.
Managing the counter state and logic is now the responsibility of the custom hook.
The same hook could be reused in the application that was keeping track of the number of clicks made to the left and right buttons:
const App = () => {
const left = useCounter()
const right = useCounter()
return (
<div>
{left.value}
<button onClick={left.increase}>
left
</button>
<button onClick={right.increase}>
right
</button>
{right.value}
</div>
)
}
The application creates two completely separate counters.
The first one is assigned to the variable left
and the other to the variable right
.
Dealing with forms in React can be complex. The following application presents the user with a form that requests the user to input their name, birthday, and height:
const App = () => {
const [name, setName] = useState('')
const [born, setBorn] = useState('')
const [height, setHeight] = useState('')
return (
<div>
<form>
name:
<input
type='text'
value={name}
onChange={(event) => setName(event.target.value)}
/>
<br/>
birthdate:
<input
type='date'
value={born}
onChange={(event) => setBorn(event.target.value)}
/>
<br />
height:
<input
type='number'
value={height}
onChange={(event) => setHeight(event.target.value)}
/>
</form>
<div>
{name} {born} {height}
</div>
</div>
)
}
Every field of the form has its own state.
To keep the state of the form synchronized with the data provided by the user,
we have to register an appropriate onChange
handler for each of the input
elements.
Let's define our own custom useField
hook that simplifies the state management of the form:
const useField = (type) => {
const [value, setValue] = useState('')
const onChange = (event) => {
setValue(event.target.value)
}
return {
type,
value,
onChange
}
}
The hook function receives the type of the input field as a parameter.
The function returns all of the attributes required by the input
: its type
, value
and the onChange
handler.
Here's how the hook can be used:
const App = () => {
const name = useField('text') // ...
return (
<div>
<form>
<input
type={name.type} value={name.value} onChange={name.onChange} />
// ...
</form>
</div>
)
}
Spread attributes
We could simplify things a bit further.
Since the name
object has exactly all of the attributes that the input
element expects to receive as props,
we can pass the props to the element using the
spread syntax in the following way:
<input {...name} />
As the example in the React documentation states, the following two ways of passing props to a component achieve the exact same result:
Explicit assignment | Using Spread syntax |
---|---|
<Greeting firstName='Randy' |
const person = { |
The application gets simplified into the following format:
const App = () => {
const name = useField('text') const born = useField('date') const height = useField('number')
return (
<div>
<form>
name:
<input {...name} /> <br/>
birthdate:
<input {...born} />
<br />
height:
<input {...height} />
</form>
<div>
{name.value} {born.value} {height.value} </div>
</div>
)
}
When we encapsulate the byzantine details for synchronizing the form's state inside our hook, which simplifies our form.
Custom hooks are not only a tool for reuse; they also provide a better way for dividing our code into smaller modular parts.
More about hooks
The internet is starting to fill up with more and more helpful material related to hooks. The following sources are worth checking out: