When we manage state in React, we have to store data, but we also record data dependencies. Dependencies are intrinsic to the way that React works. They allow React to update the page efficiently and only when necessary.
Managing data dependencies, then, is the key to managing state in React. You will see throughout this chapter that most of the tools and techniques we use are to ensure that we manage dependencies efficiently.
A key concept in the following recipes is a data reducer. A reducer is simply a function that receives a single object or an array and then returns a modified copy. This simple concept is what lies behind much of the state management in React. We’ll look at how React uses reducer functions natively and how we can use the Redux library to manage data application-wide with reducers.
We’ll also look at selector functions. These allow us to drill into the state returned by reducers. Selectors help us ignore the irrelevant data, and in doing so, they significantly improve the performance of our code.
Along the way, we’ll look at simple ways of checking whether you’re online, how to manage form data, and various other tips and tricks to keep your application ticking along.
Many React components are straightforward. They do little more than render a section of HTML and perhaps show a few properties.
However, some components can be more complicated. They might need to manage several pieces of internal state. For example, consider the simple number game you can see in Figure 3-1.
The component displays a series of numeric tiles, in a grid, with a single space. If the user clicks a tile next to the space, they can move it. In this way, the user can rearrange the tiles until they are in the correct order from 1 to 8.
This component renders a small amount of HTML, but it will require some fairly complex logic and data. It will record the positions of the tiles. It will need to know whether a user can move a given tile. It will need to know how to move the tile. It will need to know whether the game is complete. It will also need to do other things, such as reset the game by shuffling the tiles.
It’s entirely possible to write all this code inside the component, but it will be harder to test it. You could use the React Testing Library, but that is probably overkill, given that most of the code will have very little to do with rendering HTML.
If you have a component with some complex internal state or that needs to manipulate its state in complex ways, consider using a reducer.
A reducer is a function that accepts two parameters:
An object or array that represents a given state
An action that describes how you want to modify the state
The function returns a new copy of the state we pass to it.
The action parameter can be whatever you want, but typically it is an object with a string type
attribute and a payload
with
additional information. You can think of the type
as a command name and the payload
as parameters to the command.
For example, if we number our tile positions from 0 (top-left) to 8 (bottom-right), we might tell the reducer to move whatever tile is in the top-left corner with:
{
type
:
'move'
,
payload
:
0
}
We need an object or array that completely defines our game’s internal state. We could use a simple array of strings:
[
'1'
,
'2'
,
'3'
,
null
,
'5'
,
'6'
,
'7'
,
'8'
,
'4'
]
That would represent the tiles laid out like this:
1 |
2 |
3 |
5 |
6 |
|
7 |
8 |
4 |
However, a slightly more flexible approach uses an object for our state and gives it an items
attribute containing the current
tile layout:
{
items
:
[
'1'
,
'2'
,
'3'
,
null
,
'5'
,
'6'
,
'7'
,
'8'
,
'4'
]
}
Why would we do this? Because it will allow our reducer to return other state values, such as whether or not the game is complete:
{
items
:
[
'1'
,
'2'
,
'3'
,
'4'
,
'5'
,
'6'
,
'7'
,
'8'
,
null
],
complete
:
true
}
We’ve decided on an action (move
) and know how the state will be structured, which means we’ve done enough design to create a
test:
import
reducer
from
'./reducer'
describe
(
'reducer'
,
()
=>
{
it
(
'should be able to move 1 down if gap below'
,
()
=>
{
let
state
=
{
items
:
[
'1'
,
'2'
,
'3'
,
null
,
'5'
,
'6'
,
'7'
,
'8'
,
'4'
],
}
state
=
reducer
(
state
,
{
type
:
'move'
,
payload
:
0
})
expect
(
state
.
items
).
toEqual
([
null
,
'2'
,
'3'
,
'1'
,
'5'
,
'6'
,
'7'
,
'8'
,
'4'
,
])
})
it
(
'should say when it is complete'
,
()
=>
{
let
state
=
{
items
:
[
'1'
,
'2'
,
'3'
,
'4'
,
'5'
,
'6'
,
'7'
,
null
,
'8'
],
}
state
=
reducer
(
state
,
{
type
:
'move'
,
payload
:
8
})
expect
(
state
.
complete
).
toBe
(
true
)
state
=
reducer
(
state
,
{
type
:
'move'
,
payload
:
5
})
expect
(
state
.
complete
).
toBe
(
false
)
})
})
In our first test scenario, we pass in the tiles’ locations in one state. Then we check that the reducer returns the tiles in a new state.
In our second test, we perform two tile moves and then look for a complete
attribute to tell us the game has ended.
OK, we’ve delayed looking at the actual reducer code long enough:
function
trySwap
(
newItems
,
position
,
t
)
{
if
(
newItems
[
t
]
===
null
)
{
const
temp
=
newItems
[
position
]
newItems
[
position
]
=
newItems
[
t
]
newItems
[
t
]
=
temp
}
}
function
arraysEqual
(
a
,
b
)
{
for
(
let
i
=
0
;
i
<
a
.
length
;
i
++
)
{
if
(
a
[
i
]
!==
b
[
i
])
{
return
false
}
}
return
true
}
const
CORRECT
=
[
'1'
,
'2'
,
'3'
,
'4'
,
'5'
,
'6'
,
'7'
,
'8'
,
null
]
function
reducer
(
state
,
action
)
{
switch
(
action
.
type
)
{
case
'move'
:
{
const
position
=
action
.
payload
const
newItems
=
[...
state
.
items
]
const
col
=
position
%
3
if
(
position
<
6
)
{
trySwap
(
newItems
,
position
,
position
+
3
)
}
if
(
position
>
2
)
{
trySwap
(
newItems
,
position
,
position
-
3
)
}
if
(
col
<
2
)
{
trySwap
(
newItems
,
position
,
position
+
1
)
}
if
(
col
>
0
)
{
trySwap
(
newItems
,
position
,
position
-
1
)
}
return
{
...
state
,
items
:
newItems
,
complete
:
arraysEqual
(
newItems
,
CORRECT
),
}
}
default
:
{
throw
new
Error
(
'Unknown action: '
+
action
.
type
)
}
}
}
export
default
reducer
Our reducer currently recognizes a single action: move
. The code in our GitHub repository also includes actions for shuffle
and reset
. The repository also has a more exhaustive set of tests that we used to create the previous code.
But none of this code includes any React components. It’s pure JavaScript and so can be created and tested in isolation from the outside world.
Be careful to generate a new object in the reducer to represent the new state. Doing so ensures each new state completely independent of those that came before it.
Now it’s time to wire up our reducer into a React component, with the useReducer
hook:
import
{
useReducer
}
from
'react'
import
reducer
from
'./reducer'
import
'./Puzzle.css'
const
Puzzle
=
()
=>
{
const
[
state
,
dispatch
]
=
useReducer
(
reducer
,
{
items
:
[
'4'
,
'1'
,
'2'
,
'7'
,
'6'
,
'3'
,
null
,
'5'
,
'8'
],
})
return
(
<
div
className
=
"Puzzle"
>
<
div
className
=
"Puzzle-squares"
>
{
state
.
items
.
map
((
s
,
i
)
=>
(
<
div
className
=
{
`
Puzzle
-
square
$
{
s
?
''
:
'Puzzle-square-empty'
}
`
}
key
=
{
`
square
-
$
{
i
}
`
}
onClick
=
{()
=>
dispatch
({
type
:
'move'
,
payload
:
i
})}
>
{
s
}
</
div
>
))}
</
div
>
<
div
className
=
"Puzzle-controls"
>
<
button
className
=
"Puzzle-shuffle"
onClick
=
{()
=>
dispatch
({
type
:
'shuffle'
})}
>
Shuffle
</
button
>
<
button
className
=
"Puzzle-reset"
onClick
=
{()
=>
dispatch
({
type
:
'reset'
})}
>
Reset
</
button
>
</
div
>
{
state
.
complete
&&
(
<
div
className
=
"Puzzle-complete"
>
Complete
!
</
div
>
)}
</
div
>
)
}
export
default
Puzzle
Even though our puzzle component is doing something quite complicated, that actual React code is relatively short.
The useReducer
accepts a reducer function and a starting state, and it returns a two-element array:
The first element in the array is the current state from the reducer
The second element is a dispatch
function that allows us to send actions to the reducer.
We display the tiles by looping through the strings in the array given by state.items
.
If someone clicks a tile at position i
, we send a move
command to the reducer:
onClick
=
{()
=>
dispatch
({
type
:
'move'
,
payload
:
i
})}
The React component has no idea what it takes to move the tile. It doesn’t even know if it can move the tile at all. The component sends the action to the reducer.
If the move
action moves a tile, the component will automatically re-render the component with the tiles in their new positions.
If the game is complete, the component will know by the value of state.complete
:
state
.
complete
&&
<
div
className
=
'Puzzle-complete'
>
Complete
!
</
div
>
We also added two buttons to run the shuffle
and reset
actions, which we omitted earlier but is in the GitHub repository.
Now that we’ve created our component, let’s try it. When we first load the component, we see it in its initial state, as shown in Figure 3-2.
If we click the tile labeled 7, it moves into the gap (see Figure 3-3).
If we click the Shuffle button, the reducer rearranges tiles randomly, as shown in Figure 3-4.
And if we click Reset, the puzzle changes to the completed position, and the “Complete!” text appears (see Figure 3-5).
We bury all of the complexity inside the reducer function, where we can test it, and the component is simple and easy to maintain.
Reducers are a way of managing complexity. You will typically use a reducer in either of these cases:
You have a large amount of internal state to manage.
You need complex logic to manage the internal state of your component.
If either of these things is correct, then a reducer can make your code significantly easier to manage.
However, be wary of using reducers for very small components. If your component has a simple state and little logic, you probably don’t need the added complexity of a reducer.
Sometimes, even if you do have a complex state, there are alternative approaches. For example, if you are capturing and validating data in a form, it might be better to create a validating form component (see “Create and Validate Forms”).
You need to ensure that your reducer does not have any side effects. Avoid, say, making network calls that update a server. If your reducer has side effects, there is every chance that it might break. React (sneakily) might sometimes make additional calls to your reducer in development mode to make sure that no side effects are happening. If you’re using a reducer and notice that React calls your code twice when rendering a component, it means React is checking for bad behavior.
With all of those provisos, reducers are an excellent tool at fighting complexity. They are integral to libraries such as Redux, can easily be reused and combined, simplify components, and make your React code significantly easier to test.
You can download the source for this recipe from the GitHub site.
Part of the promise of JavaScript-rich frameworks like React is that web applications can closely resemble desktop applications. One common feature in desktop applications is the ability to undo an action. Some native components within React applications automatically support an undo function. If you edit some text in a text area, and then press Cmd/Ctrl-Z, it will undo your edit. But what about extending undo into custom components? How is it possible to track state changes without a large amount of code?
If a reducer function manages the state in your component, you can implement a quite general undo function using an undo-reducer.
Consider this piece of code from the Puzzle
example from “Use Reducers to Manage Complex State”:
const
[
state
,
dispatch
]
=
useReducer
(
reducer
,
{
items
:
[
'4'
,
'1'
,
'2'
,
'7'
,
'6'
,
'3'
,
null
,
'5'
,
'8'
],
})
This code uses a reducer function (called reducer
) and an initial state to manage the tiles in a number-puzzle game (see Figure 3-6).
If the user clicks the Shuffle button, the component updates the tile state by sending a shuffle
action to the reducer:
<
button
className
=
'Puzzle-shuffle'
onClick
=
{()
=>
dispatch
({
type
:
'shuffle'
})}>
Shuffle
</
button
>
(For more details on what reducers are and when you should use them, see “Use Reducers to Manage Complex State”.)
We will create a new hook called useUndoReducer
, which is a drop-in replacement for useReducer
:
const
[
state
,
dispatch
]
=
useUndoReducer
(
reducer
,
{
items
:
[
'4'
,
'1'
,
'2'
,
'7'
,
'6'
,
'3'
,
null
,
'5'
,
'8'
],
})
The useUndoReducer
hook will magically give our component the ability to go back in time:
<
button
className
=
"Puzzle-undo"
onClick
=
{()
=>
dispatch
({
type
:
'undo'
})}
>
Undo
</
button
>
If we add this button to the component, it undoes the last action the user performed, as shown in Figure 3-7.
But how do we perform this magic? Although useUndoReducer
is relatively easy to use, it’s somewhat harder to understand. But it’s
worth doing so that you can adjust the recipe to your requirements.
We can take advantage of the fact that all reducers work in the same way:
The action defines what you want to do.
The reducer returns a fresh state after each action.
No side effects are allowed when calling the reducer.
Also, reducers are just simple JavaScript functions that accept a state object and an action object.
Because reducers work in such a well-defined way, we can create a new reducer (an undo-reducer) that wraps around another reducer function. Our undo-reducer will work as an intermediary. It will pass most actions through to the underlying reducer while keeping a history of all previous states. If someone wants to undo an action, it will find the last state from its history and then return that without calling the underlying reducer.
We’ll begin by creating a higher-order function that accepts one reducer and returns another:
import
lodash
from
'lodash'
const
undo
=
(
reducer
)
=>
(
state
,
action
)
=>
{
let
{
undoHistory
=
[],
undoActions
=
[],
...
innerState
}
=
lodash
.
cloneDeep
(
state
)
switch
(
action
.
type
)
{
case
'undo'
:
{
if
(
undoActions
.
length
>
0
)
{
undoActions
.
pop
()
innerState
=
undoHistory
.
pop
()
}
break
}
case
'redo'
:
{
if
(
undoActions
.
length
>
0
)
{
undoHistory
=
[...
undoHistory
,
{
...
innerState
}]
undoActions
=
[
...
undoActions
,
undoActions
[
undoActions
.
length
-
1
],
]
innerState
=
reducer
(
innerState
,
undoActions
[
undoActions
.
length
-
1
]
)
}
break
}
default
:
{
undoHistory
=
[...
undoHistory
,
{
...
innerState
}]
undoActions
=
[...
undoActions
,
action
]
innerState
=
reducer
(
innerState
,
action
)
}
}
return
{
...
innerState
,
undoHistory
,
undoActions
}
}
export
default
undo
This reducer is quite a complex function, so it’s worth taking some time to understand what it does.
It creates a reducer function that keeps track of the actions and states we pass to it. Let’s say our game component sends an action
to shuffle the tiles in the game. Our reducer will first check if the action has the type undo
or redo
. It doesn’t. So it passes
the shuffle
action to the underlying reducer that manages the tiles in our game (see Figure 3-8).
As it passes the shuffle
action through to the underlying reducer, the undo
code keeps track of the existing state and the
shuffle
action by adding them to the undoHistory
and undoActions
. It then returns the state of the underlying game reducer and
the undoHistory
and undoActions
.
If our puzzle component sends in an undo
action, the undo-reducer returns the previous state from the undoHistory
, completely
bypassing the game’s own reducer function (see Figure 3-9).
Now let’s look at the useUndoReducer
hook itself:
import
{
useReducer
}
from
'react'
import
undo
from
'./undo'
const
useUndoReducer
=
(
reducer
,
initialState
)
=>
useReducer
(
undo
(
reducer
),
initialState
)
export
default
useUndoReducer
This useUndoReducer
hook is a concise piece of code. It’s simply a call to the built-in useReducer
hook, but instead of passing
the reducer straight through, it passes undo(reducer)
. The result is that your component uses an enhanced version of the reducer
you provide: one that can undo and redo actions.
Here is our updated Puzzle
component (see “Use Reducers to Manage Complex State” for the original version):
import
reducer
from
'./reducer'
import
useUndoReducer
from
'./useUndoReducer'
import
'./Puzzle.css'
const
Puzzle
=
()
=>
{
const
[
state
,
dispatch
]
=
useUndoReducer
(
reducer
,
{
items
:
[
'4'
,
'1'
,
'2'
,
'7'
,
'6'
,
'3'
,
null
,
'5'
,
'8'
],
})
return
(
<
div
className
=
"Puzzle"
>
<
div
className
=
"Puzzle-squares"
>
{
state
.
items
.
map
((
s
,
i
)
=>
(
<
div
className
=
{
`
Puzzle
-
square
$
{
s
?
''
:
'Puzzle-square-empty'
}
`
}
key
=
{
`
square
-
$
{
i
}
`
}
onClick
=
{()
=>
dispatch
({
type
:
'move'
,
payload
:
i
})}
>
{
s
}
</
div
>
))}
</
div
>
<
div
className
=
"Puzzle-controls"
>
<
button
className
=
"Puzzle-shuffle"
onClick
=
{()
=>
dispatch
({
type
:
'shuffle'
})}
>
Shuffle
</
button
>
<
button
className
=
"Puzzle-reset"
onClick
=
{()
=>
dispatch
({
type
:
'reset'
})}
>
Reset
</
button
>
</
div
>
<
div
className
=
"Puzzle-controls"
>
<
button
className
=
"Puzzle-undo"
onClick
=
{()
=>
dispatch
({
type
:
'undo'
})}
>
Undo
</
button
>
<
button
className
=
"Puzzle-redo"
onClick
=
{()
=>
dispatch
({
type
:
'redo'
})}
>
Redo
</
button
>
</
div
>
{
state
.
complete
&&
(
<
div
className
=
"Puzzle-complete"
>
Complete
!
</
div
>
)}
</
div
>
)
}
export
default
Puzzle
The only changes are that we use useUndoReducer
instead of useReducer
, and we’ve added a couple of buttons to call the “undo”
and “redo” actions.
If you now load the component and makes some changes, you can undo the changes one at a time, as shown in Figure 3-10.
The undo-reducer shown here will work with reducers that accept and return state objects. If your reducer manages state using
arrays, you will have to modify the undo
function.
Because it keeps a history of all previous states, you probably want to avoid using it if your state data is extensive or if you’re using it in circumstances where it might make a huge number of changes. Otherwise, you might want to limit the maximum size of the history.
Also, bear in mind that it maintains its history in memory. If a user reloads the entire page, then the history will disappear. It should be possible to resolve this issue by persisting the global state in local storage whenever it changes.
You can download the source for this recipe from the GitHub site.
Most React applications use forms to some degree, and most applications take an ad-hoc approach to creating them. If a team is building your application, you might find that some developers manage individual fields in separate state variables. Others will choose to record form state in a single-value object, which is simpler to pass into and out of the form but can be tricky for each field to update. Field validation often leads to spaghetti code, with some forms validating at submit time and others validating dynamically as the user types. Some forms might show validation messages when the form first loads. In other forms, the messages might appear only after the user has touched the fields.
These variations in design can lead to poor user experience and an inconsistent approach to writing code. In our experience working with React teams, forms and form validation are common stumbling blocks for developers.
To apply some consistency to form development, we will create a SimpleForm
component that we will wrap around one or more
InputField
components. This is an example of the use of SimpleForm
and InputField
:
import
{
useEffect
,
useState
}
from
'react'
import
'./App.css'
import
SimpleForm
from
'./SimpleForm'
import
InputField
from
'./InputField'
const
FormExample0
=
({
onSubmit
,
onChange
,
initialValue
=
{}
})
=>
{
const
[
formFields
,
setFormFields
]
=
useState
(
initialValue
)
const
[
valid
,
setValid
]
=
useState
(
true
)
const
[
errors
,
setErrors
]
=
useState
({})
useEffect
(()
=>
{
if
(
onChange
)
{
onChange
(
formFields
,
valid
,
errors
)
}
},
[
onChange
,
formFields
,
valid
,
errors
])
return
(
<
div
className
=
"TheForm"
>
<
h1
>
Single
field
</
h1
>
<
SimpleForm
value
=
{
formFields
}
onChange
=
{
setFormFields
}
onValid
=
{(
v
,
errs
)
=>
{
setValid
(
v
)
setErrors
(
errs
)
}}
>
<
InputField
name
=
"field1"
onValidate
=
{(
v
)
=>
!
v
||
v
.
length
<
3
?
'Too short!'
:
null
}
/>
<
button
onClick
=
{()
=>
onSubmit
&&
onSubmit
(
formFields
)}
disabled
=
{
!
valid
}
>
Submit
!
</
button
>
</
SimpleForm
>
</
div
>
)
}
export
default
FormExample0
We track the state of the form in a single object, formFields
. Whenever we change a field in the form, the field will call
onChange
on the SimpleForm
. The field1
field is validated using the onValidate
method, and whenever the validation state
changes, the field calls the onValid
method on the SimpleForm
. Validation will occur only if the user has interacted with a
field: making it dirty.
You can see the form running in Figure 3-11.
There is no need to track individual field values. The form value object records individual field values with attributes derived
from the name of the field. The InputField
handles the details of when to run the validation: it will update the form value and
decide when to display errors.
Figure 3-12 shows a slightly more complex example that uses the SimpleForm
with several fields.
To create the SimpleForm
and InputField
components, we must first look at how they will communicate with each other. An
InputField
component will need to tell the SimpleForm
when its value has changed and whether or not the new value is valid. It
will do this with a context.
A context is a storage scope. When a component stores values in a context, that value is visible to its subcomponents. The
SimpleForm
will create a context called FormContext
and use it to store a set of callback functions that any child component can
use to communicate with the form:
import
{
createContext
}
from
'react'
const
FormContext
=
createContext
({})
export
default
FormContext
To see how SimpleForm
works, let’s begin with a simplified version, which tracks only its subcomponents’ values, without worrying
about validation just yet:
import
React
,
{
useCallback
,
useEffect
,
useState
}
from
'react'
import
'./SimpleForm.css'
import
FormContext
from
'./FormContext'
function
updateWith
(
oldValue
,
field
,
value
)
{
const
newValue
=
{
...
oldValue
}
newValue
[
field
]
=
value
return
newValue
}
const
SimpleForm
=
({
children
,
value
,
onChange
,
onValid
})
=>
{
const
[
values
,
setValues
]
=
useState
(
value
||
{})
useEffect
(()
=>
{
setValues
(
value
||
{})
},
[
value
])
useEffect
(()
=>
{
if
(
onChange
)
{
onChange
(
values
)
}
},
[
onChange
,
values
])
let
setValue
=
useCallback
(
(
field
,
v
)
=>
setValues
((
vs
)
=>
updateWith
(
vs
,
field
,
v
)),
[
setValues
]
)
let
getValue
=
useCallback
((
field
)
=>
values
[
field
],
[
values
])
let
form
=
{
setValue
:
setValue
,
value
:
getValue
,
}
return
(
<
div
className
=
"SimpleForm-container"
>
<
FormContext
.
Provider
value
=
{
form
}>
{
children
}
</
FormContext
.
Provider
>
</
div
>
)
}
export
default
SimpleForm
The final version of SimpleForm
will have additional code for tracking validation and errors, but this cut-down form is easier to
understand.
The form is going to track all of its field values in the values
object. The form creates two callback functions called getValue
and setValue
and puts them into the context (as the form
object), where subcomponents will find them. We put the form
into
the context by wrapping a <FormContext.Provider>
around the child components.
Notice that we have wrapped the getValue
and setValue
callbacks in useCallback
, which prevents the component from creating a
new version of each function every time we render the SimpleForm
.
Whenever a child component calls the form.value()
function, it will receive the current value of the specified field. If a child
component calls form.setValue()
, it will update that value.
Now let’s look at a simplified version of the InputField
component, again with any validation code removed to make it easier to
understand:
import
React
,
{
useContext
}
from
'react'
import
FormContext
from
'./FormContext'
import
'./InputField.css'
const
InputField
=
(
props
)
=>
{
const
form
=
useContext
(
FormContext
)
if
(
!
form
.
value
)
{
return
'InputField should be wrapped in a form'
}
const
{
name
,
label
,
...
otherProps
}
=
props
const
value
=
form
.
value
(
name
)
return
(
<
div
className
=
"InputField"
>
<
label
htmlFor
=
{
name
}>{
label
||
name
}
:
</
label
>
<
input
id
=
{
name
}
value
=
{
value
||
''
}
onChange
=
{(
event
)
=>
{
form
.
setValue
(
name
,
event
.
target
.
value
)
}}
{
...otherProps
}
/>{
' '
}
{}
</
div
>
)
}
export
default
InputField
The InputField
extracts the form
object from the FormContext
. If it cannot find a form
object, it knows that we have not
wrapped it in a SimpleForm
component. The InputField
then renders an input
field, setting its value to whatever is returned by
form.value(name)
. If the user changes the field’s value, the InputField
component sends the new value to
form.setValue(name, event.target.value)
.
If you need a form field other than an input
, you can wrap it in some component similar to the InputField
shown here.
The validation code is just more of the same. In the same way that the form tracks its current value in the values
state, it also
needs to track which fields are dirty and which are invalid. It then needs to pass callbacks for setDirty
, isDirty
, and
setInvalid
. These callbacks are used by the child fields when running their
onValidate
code.
Here is the final version of the SimpleForm
component, including validation:
import
{
useCallback
,
useEffect
,
useState
}
from
'react'
import
FormContext
from
'./FormContext'
import
'./SimpleForm.css'
const
SimpleForm
=
({
children
,
value
,
onChange
,
onValid
})
=>
{
const
[
values
,
setValues
]
=
useState
(
value
||
{})
const
[
dirtyFields
,
setDirtyFields
]
=
useState
({})
const
[
invalidFields
,
setInvalidFields
]
=
useState
({})
useEffect
(()
=>
{
setValues
(
value
||
{})
},
[
value
])
useEffect
(()
=>
{
if
(
onChange
)
{
onChange
(
values
)
}
},
[
onChange
,
values
])
useEffect
(()
=>
{
if
(
onValid
)
{
onValid
(
Object
.
keys
(
invalidFields
).
every
((
i
)
=>
!
invalidFields
[
i
]),
invalidFields
)
}
},
[
onValid
,
invalidFields
])
const
setValue
=
useCallback
(
(
field
,
v
)
=>
setValues
((
vs
)
=>
({
...
vs
,
[
field
]
:
v
})),
[
setValues
]
)
const
getValue
=
useCallback
((
field
)
=>
values
[
field
],
[
values
])
const
setDirty
=
useCallback
(
(
field
)
=>
setDirtyFields
((
df
)
=>
({
...
df
,
[
field
]
:
true
})),
[
setDirtyFields
]
)
const
getDirty
=
useCallback
(
(
field
)
=>
Object
.
keys
(
dirtyFields
).
includes
(
field
),
[
dirtyFields
]
)
const
setInvalid
=
useCallback
(
(
field
,
error
)
=>
{
setInvalidFields
((
i
)
=>
({
...
i
,
[
field
]
:
error
?
error
:
undefined
,
}))
},
[
setInvalidFields
]
)
const
form
=
{
setValue
:
setValue
,
value
:
getValue
,
setDirty
:
setDirty
,
isDirty
:
getDirty
,
setInvalid
:
setInvalid
,
}
return
(
<
div
className
=
"SimpleForm-container"
>
<
FormContext
.
Provider
value
=
{
form
}>
{
children
}
</
FormContext
.
Provider
>
</
div
>
)
}
export
default
SimpleForm
And this is the final version of the InputField
component. Notice that the field is counted as dirty once it loses focus or its
value changes:
import
{
useContext
,
useEffect
,
useState
}
from
'react'
import
FormContext
from
'./FormContext'
import
'./InputField.css'
const
splitCamelCase
=
(
s
)
=>
s
.
replace
(
/([a-z0-9])([A-Z0-9])/g
,
'$1 $2'
)
.
replace
(
/^([a-z])/
,
(
x
)
=>
x
.
toUpperCase
())
const
InputField
=
(
props
)
=>
{
const
form
=
useContext
(
FormContext
)
const
[
error
,
setError
]
=
useState
(
''
)
const
{
onValidate
,
name
,
label
,
...
otherProps
}
=
props
let
value
=
form
.
value
&&
form
.
value
(
name
)
useEffect
(()
=>
{
if
(
onValidate
)
{
setError
(
onValidate
(
value
))
}
},
[
onValidate
,
value
])
const
setInvalid
=
form
.
setInvalid
useEffect
(()
=>
{
if
(
setInvalid
)
{
setInvalid
(
name
,
error
)
}
},
[
setInvalid
,
name
,
error
])
if
(
!
form
.
value
)
{
return
'InputField should be wrapped in a form'
}
return
(
<
div
className
=
"InputField"
>
<
label
htmlFor
=
{
name
}>{
label
||
splitCamelCase
(
name
)}
:
</
label
>
<
input
id
=
{
name
}
onBlur
=
{()
=>
form
.
setDirty
(
name
)}
value
=
{
value
||
''
}
onChange
=
{(
event
)
=>
{
form
.
setDirty
(
name
)
form
.
setValue
(
name
,
event
.
target
.
value
)
}}
{
...otherProps
}
/>{
' '
}
{
<
div
className
=
"InputField-error"
>
{
form
.
isDirty
(
name
)
&&
error
?
error
:
<>
&
nbsp
;</>}
</
div
>
}
</
div
>
)
}
export
default
InputField
You can use this recipe to create many simple forms, and you can extend it for use with any React component. For example, if you are
using a third-party calendar or date picker, you would only need to wrap it in a component similar to InputField
to use it inside
a SimpleForm
.
This recipe doesn’t support forms within forms or arrays of forms. It should be possible to modify the SimpleForm
component to
behave like an InputField
to place one form inside another.
You can download the source for this recipe from the GitHub site.
Sometimes a React application needs to respond to the time of day. It might only need to display the current time, or it might need to poll a server at regular intervals or change its interface as day turns to night. But how do you cause your code to re-render as the result of a time change? How do you avoid rendering components too often? And how do you do all that without overcomplicating your code?
We’re going to create a useClock
hook. The useClock
hook will give us access to a formatted version of the current date and time
and automatically update the interface when the time changes. Here’s an example of the code in use, and Figure 3-13 shows it
running:
import
{
useEffect
,
useState
}
from
'react'
import
useClock
from
'./useClock'
import
ClockFace
from
'./ClockFace'
import
'./Ticker.css'
const
SimpleTicker
=
()
=>
{
const
[
isTick
,
setTick
]
=
useState
(
false
)
const
time
=
useClock
(
'HH:mm:ss'
)
useEffect
(()
=>
{
setTick
((
t
)
=>
!
t
)
},
[
time
])
return
(
<
div
className
=
"Ticker"
>
<
div
className
=
"Ticker-clock"
>
<
h1
>
Time
{
isTick
?
'Tick!'
:
'Tock!'
}</
h1
>
{
time
}
<
br
/>
<
ClockFace
time
=
{
time
}
/>
</
div
>
</
div
>
)
}
export
default
SimpleTicker
SimpleTicker
over three secondsThe time
variable contains the current time in the format HH:mm:ss
. When the time changes, the value of the isTick
state is
toggled between true and false and then used to display the word Tick! or Tock! We show the current time and then also display
the time with a ClockFace
component.
As well as accepting a date and time format, useClock
can take a number specifying the number of milliseconds between updates
(see Figure 3-14):
import
{
useEffect
,
useState
}
from
'react'
import
useClock
from
'./useClock'
import
'./Ticker.css'
const
IntervalTicker
=
()
=>
{
const
[
isTick3
,
setTick3
]
=
useState
(
false
)
const
tickThreeSeconds
=
useClock
(
3000
)
useEffect
(()
=>
{
setTick3
((
t
)
=>
!
t
)
},
[
tickThreeSeconds
])
return
(
<
div
className
=
"Ticker"
>
<
div
className
=
"Ticker-clock"
>
<
h1
>{
isTick3
?
'3 Second Tick!'
:
'3 Second Tock!'
}</
h1
>
{
tickThreeSeconds
}
</
div
>
</
div
>
)
}
export
default
IntervalTicker
IntervalTicker
re-renders the component every three secondsThis version is more useful if you want to perform some task at regular intervals, such as polling a network service.
To poll a network service, consider using a clock with “Convert Network Calls to Hooks”. If the current value of the clock is passed as a dependency to a hook that makes network calls, the network call will be repeated every time the clock changes.
If you pass a numeric parameter to useClock
, it will return a time string in ISO format like 2021-06-11T14:50:34.706
.
To build this hook, we will use a third-party library called Moment.js to handle date and time formatting. If you would prefer to use another library, such as Day.js, it should be straightforward to convert:
$ npm install moment
This is the code for useClock
:
import
{
useEffect
,
useState
}
from
'react'
import
moment
from
'moment'
const
useClock
=
(
formatOrInterval
)
=>
{
const
format
=
typeof
formatOrInterval
===
'string'
?
formatOrInterval
:
'YYYY-MM-DDTHH:mm:ss.SSS'
const
interval
=
typeof
formatOrInterval
===
'number'
?
formatOrInterval
:
500
const
[
response
,
setResponse
]
=
useState
(
moment
(
new
Date
()).
format
(
format
)
)
useEffect
(()
=>
{
const
newTimer
=
setInterval
(()
=>
{
setResponse
(
moment
(
new
Date
()).
format
(
format
))
},
interval
)
return
()
=>
clearInterval
(
newTimer
)
},
[
format
,
interval
])
return
response
}
export
default
useClock
We derive the date and time format
and the required ticking interval
from the
formatOrInterval
parameter passed to the hook. Then we create a timer with
setInterval
. This time will set the response
value every interval
milliseconds. When we set
the response
string to a new time, any component that relies on useClock
will re-render.
We need to make sure that we cancel any timers that are no longer in use. We can do this using a feature of the useEffect
hook. If
we return a function at the end of our useEffect
code, then that function will be called the next time useEffect
needs to run.
So, we can use it to clear the old timer before creating a new one.
If we pass a new format or interval to useClock
, it will cancel its old timer and respond using a new timer.
This recipe is an example of how you can use hooks to solve a simple problem simply. React code (the clue is in the name) reacts to
dependency changes. Instead of thinking, “How can I run this piece of code every second?” the useClock
hook allows you to write
code that depends on the current time and hides away all of the gnarly details of creating timers, updating state, and clearing
timers.
If you use the useClock
hook several times in a component, then a time change can result in multiple renders. For example, if you
have two clocks that format the current time in 12-hour format (04:45) and 24-hour format (16:45), then your component will render
twice when the minute changes. An extra render once a minute is unlikely to have much of a performance impact.
You can also use the useClock
hook inside other hooks. If you create a useMessages
hook to retrieve messages from a server, you
can call useClock
inside it to poll the server at regular intervals.
You can download the source for this recipe from the GitHub site.
Let’s say someone is using your application on their cell phone, and then they head into a subway with no data connection. How can you check that the network connection has disappeared? What’s a React-friendly way of updating your interface to either tell the user that there’s a problem or disable some features that require network access?
We will create a hook called useOnline
that will tell us whether we’re connected to a network. We need code that runs when the
browser loses or regains a connection to the network. Fortunately, there are window/body-level events called online
and
offline
that do exactly that. When the online
and offline
events are triggered, the current network state will be given by
navigator.onLine
, which will be set to true
or false
:
import
{
useEffect
,
useState
}
from
'react'
const
useOnline
=
()
=>
{
const
[
online
,
setOnline
]
=
useState
(
navigator
.
onLine
)
useEffect
(()
=>
{
if
(
window
.
addEventListener
)
{
window
.
addEventListener
(
'online'
,
()
=>
setOnline
(
true
),
false
)
window
.
addEventListener
(
'offline'
,
()
=>
setOnline
(
false
),
false
)
}
else
{
document
.
body
.
ononline
=
()
=>
setOnline
(
true
)
document
.
body
.
onoffline
=
()
=>
setOnline
(
false
)
}
},
[])
return
online
}
export
default
useOnline
This hook manages its connection state in the online
variable. When the hook is first run (notice the empty dependency array), we
register listeners to the browser’s online/offline events. When either of these events occurs, we can set the value of online
to
true
or false
. If this is a change to the current value, then any component using this hook will re-render.
Here’s an example of the hook in action:
import
useOnline
from
'./useOnline'
import
'./App.css'
function
App
()
{
const
online
=
useOnline
()
return
(
<
div
className
=
"App"
>
<
h1
>
Network
Checker
</
h1
>
<
span
>
You
are
now
....
{
online
?
(
<
div
className
=
"App-indicator-online"
>
ONLINE
</
div
>
)
:
(
<
div
className
=
"App-indicator-offline"
>
OFFLINE
</
div
>
)}
</
span
>
</
div
>
)
}
export
default
App
If you run the app, the page will currently show as online. If you disconnect/reconnect your network, the message will switch to OFFLINE and then back to ONLINE (see Figure 3-15).
It’s important to note that this hook checks your browser’s connection to a network, not whether it connects to the broader Internet or your server. If you would like to check that your server is running and available, you would have to write additional code.
You can download the source for this recipe from the GitHub site.
In other recipes in this chapter, we’ve seen that you can manage complex component state with a pure JavaScript function called a reducer. Reducers simplify components and make business logic more testable.
But what if you have some data, such as a shopping basket, that needs to be accessed everywhere?
We will use the Redux library to manage the global application state. Redux uses the same reducers we can give to the React
useReducer
function, but they are used to manage a single state object for the entire application. Plus, there are many extensions
to Redux that solve common programming problems and develop and manage your application more quickly.
First, we need to install the Redux library:
$ npm install redux
We will also install the React Redux library, which will make Redux far easier to use with React:
$ npm install react-redux
We’re going to use Redux to build an application containing a shopping basket (see Figure 3-16).
If a customer clicks a Buy button, the application adds the product to the basket. If they click the Buy button again, the quantity in the basket is updated. The basket will appear in several places across the application, so it’s a good candidate for moving to Redux. Here is the reducer function that we will use to manage the basket:
const
reducer
=
(
state
=
{},
action
=
{})
=>
{
switch
(
action
.
type
)
{
case
'buy'
:
{
const
basket
=
state
.
basket
?
[...
state
.
basket
]
:
[]
const
existing
=
basket
.
findIndex
(
(
item
)
=>
item
.
productId
===
action
.
payload
.
productId
)
if
(
existing
!==
-
1
)
{
basket
[
existing
].
quantity
=
basket
[
existing
].
quantity
+
1
}
else
{
basket
.
push
({
quantity
:
1
,
...
action
.
payload
})
}
return
{
...
state
,
basket
,
}
}
case
'clearBasket'
:
{
return
{
...
state
,
basket
:
[],
}
}
default
:
return
{
...
state
}
}
}
export
default
reducer
We are creating a single reducer here. Once your application grows in size, you will probably want to split your reducer into
smaller reducers, which you can combine with the Redux combineReducers
function.
The reducer function responds to buy
and clearBasket
actions. The buy
action will either add a new item to the basket or
update the quantity of an existing item if one has a matching productId
. The clearBasket
action will set the basket back to an
empty array.
Now that we have a reducer function, we will use it to create a Redux store. The store is going to be our central repository for the shared application state. To create a store, add these two lines to some top-level component such as App.js:
import
{
createStore
}
from
'redux'
import
reducer
from
'./reducer'
const
store
=
createStore
(
reducer
)
The store needs to be available globally in the app, and to do that, we need to inject it into the context of the components that
might need it. The React Redux library provides a component to inject the store in a component context called Provider
:
<Provider
store=
{store}
>
All the components inside here can access the store</Provider>
Here is the reducer.js component from the example application, which you can find in the GitHub repository for this book:
const
reducer
=
(
state
=
{},
action
=
{})
=>
{
switch
(
action
.
type
)
{
case
'buy'
:
{
const
basket
=
state
.
basket
?
[...
state
.
basket
]
:
[]
const
existing
=
basket
.
findIndex
(
(
item
)
=>
item
.
productId
===
action
.
payload
.
productId
)
if
(
existing
!==
-
1
)
{
basket
[
existing
].
quantity
=
basket
[
existing
].
quantity
+
1
}
else
{
basket
.
push
({
quantity
:
1
,
...
action
.
payload
})
}
return
{
...
state
,
basket
,
}
}
case
'clearBasket'
:
{
return
{
...
state
,
basket
:
[],
}
}
default
:
return
{
...
state
}
}
}
export
default
reducer
Now that the store is available to our components, how do we use it? React Redux allows you to access the store through hooks. If
you want to read the contents of the global state, you can use useSelector
:
const
basket
=
useSelector
((
state
)
=>
state
.
basket
)
The useSelector
hook accepts a function to extract part of the central state. Selectors are pretty efficient and will cause
your component to re-render only if the particular part of the state you are interested in changes.
If you need to submit an action to the central store, you can do it with the
useDispatch
hook:
const
dispatch
=
useDispatch
()
This returns a dispatch
function that you can use to send actions to the store:
dispatch
({
type
:
'clearBasket'
})
These hooks work by extracting the store from the current context. If you forget to add a Provider
to your application or try to
run useSelector
or useDispatch
outside of a Provider
context, you will get an error, as shown in Figure 3-17.
Provider
, you will get this errorThe completed Basket
component reads and clears the app-wide shopping basket:
import
{
useDispatch
,
useSelector
}
from
'react-redux'
import
'./Basket.css'
const
Basket
=
()
=>
{
const
basket
=
useSelector
((
state
)
=>
state
.
basket
)
const
dispatch
=
useDispatch
()
return
(
<
div
className
=
"Basket"
>
<
h2
>
Basket
</
h2
>
{
basket
&&
basket
.
length
?
(
<>
{
basket
.
map
((
item
)
=>
(
<
div
className
=
"Basket-item"
>
<
div
className
=
"Basket-itemName"
>{
item
.
name
}</
div
>
<
div
className
=
"Basket-itemProductId"
>
{
item
.
productId
}
</
div
>
<
div
className
=
"Basket-itemPricing"
>
<
div
className
=
"Basket-itemQuantity"
>
{
item
.
quantity
}
</
div
>
<
div
className
=
"Basket-itemPrice"
>{
item
.
price
}</
div
>
</
div
>
</
div
>
))}
<
button
onClick
=
{()
=>
dispatch
({
type
:
'clearBasket'
})}>
Clear
</
button
>
</>
)
:
(
'Empty'
)}
</
div
>
)
}
export
default
Basket
To demonstrate some code adding items to the basket, here’s a Boots
component that allows a customer to buy a selection of products:
import
{
useDispatch
}
from
'react-redux'
import
'./Boots.css'
const
products
=
[
{
productId
:
'BE8290004'
,
name
:
'Ski boots'
,
description
:
'Mondo 26.5. White.'
,
price
:
698.62
,
},
{
productId
:
'PC6310098'
,
name
:
'Snowboard boots'
,
description
:
'Mondo 27.5. Blue.'
,
price
:
825.59
,
},
{
productId
:
'RR5430103'
,
name
:
'Mountaineering boots'
,
description
:
'Mondo 27.3. Brown.'
,
price
:
634.98
,
},
]
const
Boots
=
()
=>
{
const
dispatch
=
useDispatch
()
return
(
<
div
className
=
"Boots"
>
<
h1
>
Boots
</
h1
>
<
dl
className
=
"Boots-products"
>
{
products
.
map
((
product
)
=>
(
<>
<
dt
>{
product
.
name
}</
dt
>
<
dd
>
<
p
>{
product
.
description
}</
p
>
<
p
>
$
{
product
.
price
}</
p
>
<
button
onClick
=
{()
=>
dispatch
({
type
:
'buy'
,
payload
:
product
})
}
>
Add
to
basket
</
button
>
</
dd
>
</>
))}
</
dl
>
</
div
>
)
}
export
default
Boots
These two components may appear at very different locations in the component tree, but they share the same Redux store. As soon as a
customer adds a product to the basket, the Basket
component will automatically update with the change (see Figure 3-18).
Basket
is re-renderedDevelopers often use the Redux library with the React framework. For a long time, it seemed, almost every React application included Redux by default. It’s probably true that Redux was often overused or used inappropriately. We have seen projects that have even banned local state in favor of using Redux for all state. We believe this approach is a mistake. Redux is intended for central application state management, not for simple component state. If you are storing data that is of concern to only one component, or its subcomponents, you should probably not store it in Redux.
However, if your application manages some global application state, then Redux is still the tool of choice.
You can download the source for this recipe from the GitHub site.
Redux is an excellent way of managing the application state centrally. However, it does have a small problem: when you reload the page, the entire state disappears (see Figure 3-19).
The state disappears because Redux keeps its state in memory. How do we prevent the state from disappearing?
We will use the Redux Persist library to keep a copy of the Redux state in local storage. To install Redux Persist, type the following:
$ npm install redux-persist
The first thing we need to do is create a persisted reducer, wrapped around our existing reducer:
import
storage
from
'redux-persist/lib/storage'
const
persistConfig
=
{
key
:
'root'
,
storage
,
}
const
persistedReducer
=
persistReducer
(
persistConfig
,
reducer
)
The storage
specifies where we will persist the Redux state: it will be in localStorage
by default. The persistConfig
says
that we want to keep our state in a
localStorage
item called persist:root
. When the Redux state changes, the persistedReducer
will write a copy with localStorage.setItem('persist:root', ...)
. We now need to create our Redux store with persistedReducer
:
const
store
=
createStore
(
persistedReducer
)
We need to interject the Redux Persist code between the Redux store and the code that’s accessing the Redux store. We do that with a
component called PersistGate
:
import
{
PersistGate
}
from
'redux-persist/integration/react'
import
{
persistStore
}
from
'redux-persist'
const
persistor
=
persistStore
(
store
)
...
<
Provider
store
=
{
store
}>
<
PersistGate
loading
=
{<
div
>
Loading
...</
div
>}
persistor
=
{
persistor
}>
Components
live
in
here
</
PersistGate
>
</
Provider
>
The PersistGate
must be inside the Redux Provider
and outside the components that are going to use Redux. The PersistGate
will watch for when the Redux state is lost and then reload it from localStorage
. It might take a moment to reload the data, and
if you want to show that the UI is briefly busy, you can pass a loading
component to the PersistGate
: for example, an animated
spinner. The loading component will be displayed in place of its child components when Redux is reloading. If you don’t want a
loading component, you can set it to null
.
Here is the final version of the modified App.js from the example app:
import
{
BrowserRouter
,
Route
,
Switch
}
from
'react-router-dom'
import
{
Provider
}
from
'react-redux'
import
{
createStore
}
from
'redux'
import
Menu
from
'./Menu'
import
Home
from
'./Home'
import
Boots
from
'./Boots'
import
Basket
from
'./Basket'
import
'./App.css'
import
reducer
from
'./reducer'
import
{
persistStore
,
persistReducer
}
from
'redux-persist'
import
{
PersistGate
}
from
'redux-persist/integration/react'
import
storage
from
'redux-persist/lib/storage'
const
persistConfig
=
{
key
:
'root'
,
storage
,
}
const
persistedReducer
=
persistReducer
(
persistConfig
,
reducer
)
const
store
=
createStore
(
persistedReducer
)
const
persistor
=
persistStore
(
store
)
function
App
()
{
return
(
<
div
className
=
"App"
>
<
Provider
store
=
{
store
}>
<
PersistGate
loading
=
{<
div
>
Loading
...</
div
>}
persistor
=
{
persistor
}
>
<
BrowserRouter
>
<
Menu
/>
<
Switch
>
<
Route
exact
path
=
"/"
>
<
Home
/>
</
Route
>
<
Route
path
=
"/boots"
>
<
Boots
/>
</
Route
>
</
Switch
>
<
Basket
/>
</
BrowserRouter
>
</
PersistGate
>
</
Provider
>
</
div
>
)
}
export
default
App
Now, when the user reloads the page, the Redux state survives, as shown in Figure 3-20.
The Redux Persist library is a simple way of persisting Redux state through page reloads. If you have a substantial amount of Redux
data, you will need to be careful not to break the localStorage
limit, which will vary from browser to browser but is typically
around 10 MB. However, if your Redux data is that size, you should consider offloading some of it to a server.
You can download the source for this recipe from the GitHub site.
When you extract your application state into an external object with a tool like Redux, you often need to process the data in some way before displaying it. For example, Figure 3-21 shows an application we have used in a few recipes in this chapter.
What if we want to calculate the total cost of the items in the basket and then calculate the amount of sales tax to pay? We could create a JavaScript function that reads through the basket items and calculates both, but that function would have to recalculate the values every time the basket renders. Is there a way of calculating derived values from the state that updates only when the state changes?
The Redux developers have created a library specifically designed to derive values efficiently from state objects, called
reselect
.
The reselect
library creates selector functions. A selector function takes a single parameter—a state object—and returns a
processed version.
We’ve already seen one selector in “Manage Global State with Redux”. We used it to return the current basket from the central Redux state:
const
basket
=
useSelector
((
state
)
=>
state
.
basket
)
The state => state.basket
is a selector function; it derives some value from a state object. The reselect
library creates highly
efficient selector functions that can cache their results if the state they depend upon has not changed.
To install reselect
, enter this command:
$ npm install reselect
Let’s begin by creating a selector function that will do the following:
Count the total number of items in a basket
Calculate the total cost of all of the items
We’ll call this function summarizer
. Before we go into the details of how we’ll write it, we’ll begin by writing a test that will
show what it will need to do:
it
(
'should be able to handle multiple products'
,
()
=>
{
const
actual
=
summarizer
({
basket
:
[
{
productId
:
'1234'
,
quantity
:
2
,
price
:
1.23
},
{
productId
:
'5678'
,
quantity
:
1
,
price
:
1.5
},
],
})
expect
(
actual
).
toEqual
({
itemCount
:
3
,
cost
:
3.96
})
})
So if we give it a state object, it will add up the quantities and costs and return an object containing the itemCount
and cost
.
We can create a selector function called summarizer
with the Reselect library like this:
import
{
createSelector
}
from
'reselect'
const
summarizer
=
createSelector
(
(
state
)
=>
state
.
basket
||
[],
(
basket
)
=>
({
itemCount
:
basket
.
reduce
((
i
,
j
)
=>
i
+
j
.
quantity
,
0
),
cost
:
basket
.
reduce
((
i
,
j
)
=>
i
+
j
.
quantity
*
j
.
price
,
0
),
})
)
export
default
summarizer
The createSelector
function creates a selector function based on other selector functions. Each of the parameters passed to
it—except the last parameter—should be selector functions. We are passing just one:
(
state
)
=>
state
.
basket
||
[]
This code extracts the basket from the state.
The final parameter passed to createSelector
(the combiner) is a function that derives a new value, based on the results of the
preceding selectors:
(
basket
)
=>
({
itemCount
:
basket
.
reduce
((
i
,
j
)
=>
i
+
j
.
quantity
,
0
),
cost
:
basket
.
reduce
((
i
,
j
)
=>
i
+
j
.
quantity
*
j
.
price
,
0
),
})
The basket
value is the result of running the state through the first selector.
Why on Earth would anyone create functions this way? Isn’t it way more complicated than just creating a JavaScript function manually, without the need to pass all of these functions to functions?
The answer is efficiency. Selectors will recalculate their values only when they need to. State objects can be complex and might
have dozens of attributes. But we are interested only in the contents of the basket
attribute, and we don’t want to have to
recalculate our costs if anything else changes.
What reselect
does is work out when the value it returns is likely to have changed. Let’s say we call it one time, and it
calculates the itemCount
and value
like this:
{
itemCount
:
3
,
cost
:
3.96
}
Then the user runs a bunch of commands that update personal preferences, posts a message to somebody, adds several things to their wish list, and so on.
Each of the events might update the global application state. But the next time we run the summarizer
function, it will return the
cached value that it produced before:
{
itemCount
:
3
,
cost
:
3.96
}
Why? Because it knows that this value is dependent only upon the basket
value in the global state. And if that hasn’t changed,
then it doesn’t need to recalculate the return value.
Because reselect
allows us to build selector functions from other selector functions, we could build another selector called
taxer
to calculate the basket’s sales tax:
import
{
createSelector
}
from
'reselect'
import
summarizer
from
'./summarizer'
const
taxer
=
createSelector
(
summarizer
,
(
summary
)
=>
summary
.
cost
*
0.07
)
export
default
taxer
The taxer
selector uses the value returned by the summarizer
function. It takes the cost
of the summarizer
result and
multiplies it by 7%. If the basket’s summarized total doesn’t change, then the taxer
function will not need to update its result.
Now that we have the summarizer
and taxer
selectors, we can use them inside a component, just as we would any other selector
function:
import
{
useDispatch
,
useSelector
}
from
'react-redux'
import
'./Basket.css'
import
summarizer
from
'./summarizer'
import
taxer
from
'./taxer'
const
Basket
=
()
=>
{
const
basket
=
useSelector
((
state
)
=>
state
.
basket
)
const
{
itemCount
,
cost
}
=
useSelector
(
summarizer
)
const
tax
=
useSelector
(
taxer
)
const
dispatch
=
useDispatch
()
return
(
<
div
className
=
"Basket"
>
<
h2
>
Basket
</
h2
>
{
basket
&&
basket
.
length
?
(
<>
{
basket
.
map
((
item
)
=>
(
<
div
className
=
"Basket-item"
>
<
div
className
=
"Basket-itemName"
>{
item
.
name
}</
div
>
<
div
className
=
"Basket-itemProductId"
>
{
item
.
productId
}
</
div
>
<
div
className
=
"Basket-itemPricing"
>
<
div
className
=
"Basket-itemQuantity"
>
{
item
.
quantity
}
</
div
>
<
div
className
=
"Basket-itemPrice"
>{
item
.
price
}</
div
>
</
div
>
</
div
>
))}
<
p
>{
itemCount
}
items
</
p
>
<
p
>
Total
:
$
{
cost
.
toFixed
(
2
)}</
p
>
<
p
>
Sales
tax
:
$
{
tax
.
toFixed
(
2
)}</
p
>
<
button
onClick
=
{()
=>
dispatch
({
type
:
'clearBasket'
})}>
Clear
</
button
>
</>
)
:
(
'Empty'
)}
</
div
>
)
}
export
default
Basket
When we run the code now, we see a summary at the bottom of the shopping basket, which will update whenever we buy a new product (see Figure 3-22).
The first time you meet selector functions, they can seem complicated and hard to understand. But it is worth taking the time to
understand them. There is nothing Redux-specific about them. There is no reason why you can’t also use them with non-Redux reducers.
Because they have no dependencies beyond the reselect
library itself, they are easy to unit test. We include example tests in
the code for this chapter.
You can download the source for this recipe from the GitHub site.