Reintroducing React
Reintroducing React
www.dbooks.org
Table Of Contents
Introduction
- Lifecycle Methods.
- static getDerivedStateFromProps.
- getSnapshotBeforeUpdate.
- static getDerivedStateFromError.
- componentDidCatch.
- Conclusion.
- Introduction to Context.
2
- Isolating Implementation Details.
- Conclusion.
- Conclusion
- Conclusion.
- Getting Started.
- Component Chart.
- Interactions.
www.dbooks.org
- Noting the Expected Behaviour.
- Conclusion.
- Handling Errors.
- No named exports.
- Conclusion.
- Introducing Hooks.
4
- Build Your own Hooks
- Advanced Hooks
- Introduction
- Props Collection
- Prop Getters
- State Initializers
- State Reducer
- Control Props
- Conclusion
Last Words
www.dbooks.org
Reintroducing React: Every
React Update Since v16
Demystified.
In this book, unlike any you may have come across before, I will deliver funny,
unfeigned and dead serious comic strips about every React update since v16+. It’ll
be hilarious, either intentionally or unintentionally, easy on beginners as well as
professionals, and will be very informative as a whole.
I love the tech community, but sometimes as a group, we tend to be a little narrow-
minded.
When people attempt to teach new technical concepts, they forget who they were
before they became developers and just churn out a lot of technical jargon - like
other developers they’ve seen.
When you get to know people, it turns out so many of us have different diverse
backgrounds! “If you were a comedian, why not explain technical concepts with
some comedy?
6
that personality. But instead of just talking, I want to make it noteworthy and lead
by example. So, you’re welcome to my rendition of a comic strip inspired book
about every React update since v16.
With recently released v16.8 features, there’s going to be a lot of informative comic
strips to be delivered!
Code Repository
Please find the book’s associated code repository on Github. Useful for working
alongside the examples discussed
www.dbooks.org
Why Reintroduce React ?
I wrote my first React application 3 to 4 years ago. Between then and now, the
fundamental principles of React have remained the same. React is just as
declarative and component-based today as it was then.
That’s great news, however, the way we write React applications today has
changed!
If you learned React a while back it’s not impossible that you haven’t been up to
date with every new feature/release. It’s also possible to get lost on all the new
features. Where exactly do you start? How important are they for your day to day
use?
If that’s the case with you, I hope I can provide the right help for you via this guide.
There’s a lot of state information out there. If you learn React with some old
resource, yes, you’ll learn the fundamentals of React, but modern React has new
interesting features that are worth your attention. It’s best to know those now, and
not have to unlearn and relearn newer concepts.
Whether you’ve been writing React for a while, or new to developing React
applications, I will be discussing every update to React since version 16.
This will keep you in sync with the recent changes to React, and help you write
better software.
8
On the bright side, I’m bringing you a one-stop reference to all the changes.
In this book, I’ll take you on a journey — alongside some humour and unique
content to follow.
Ready?
It goes without saying that a lot has been introduced since version 16. For ease,
each of these topics have been broken down into separate sections.
In the next section I’ll begin to discuss these changes by having a look at the new
lifecycle methods available from version 16.
www.dbooks.org
Chapter 1: New Lifecycle
Methods.
He’s been writing software for a while, but new to the React ecosystem.
Meet John.
10
For a long time he didn’t fully understand what lifecycle truly meant in the context
of React apps.
11
www.dbooks.org
The typical lifecycle for a human is something like, “child” to “adult” to “elderly”.
Here’s what a simple graphical representation for React components would be.
12
The four essential phases or lifecycle attributed to a React component include:
13
www.dbooks.org
Lifecycle Methods.
Now that you understand what lifecycle means. What are “lifecycle methods”?
Knowing the phases /lifecycle a React component goes through is one part of the
equation. The other part is understanding the methods React makes available (or
invokes) at each phase.
With the basics out of the way, let’s have a look at the actual new lifecycle methods
available from version 16.
static getDerivedStateFromProps.
Before explaining how this lifecycle method works, let me show you how the
method is used.
...
static getDerivedStateFromProps() {
14
}
...
...
And you can either return an object to update the state of the component:
...
return {
...
...
return null
...
15
www.dbooks.org
I know what you’re thinking. Why exactly is this lifecycle method important?
Well, it is one of the rarely used lifecycle methods, but it comes in handy in certain
scenarios.
Firstly, this method is called (or invoked) before the component is rendered to the
DOM on initial mount.
As you may have expected, the number of points is stored in the component state
object:
state = {
points: 10
render() {
return (
<div className="App">
<header className="App-header">
<p>
</p>
</header>
</div>
);
16
Note that the text reads, you have scored 10 points — where 10 is the number of
points in the state object.
state = {
points: 10
// *******
// NB: Not the recommended way to use this method. Just an example.
Unconditionally overriding state here is generally considered a bad idea
// ********
return {
points: 1000
17
www.dbooks.org
}
render() {
return (
<div className="App">
<header className="App-header">
<p>
</p>
</header>
</div>
);
18
With the 1000 coming from updating state within the static
getDerivedStateFromProps method.
Well, this example is contrived, and not really the way you’d use the static
getDerivedStateFromProps method. I just wanted to make sure you understood
the basics first.
With this lifecycle method, just because you can update state doesn’t mean you
should go ahead and do this. There are specific use cases for the static
getDerivedStateFromProps method, or you’ll be solving a problem with the
wrong tool.
19
www.dbooks.org
Also, component state in this manner is referred to as Derived State.
As a rule of thumb, derived state should be used sparingly as you can introduce
subtle bugs into your application if you aren’t sure of what you’re doing.
getSnapshotBeforeUpdate.
In the updating component phase, right after the render method is called, the
getSnapshotBeforeUpdate lifecycle method is called next.
This one is a little tricky, but I’ll take my time to explain how it works.
Chances are you may not always reach out for this lifecycle method, but it may
come in handy in certain special cases. Specifically when you need to grab some
information from the DOM (and potentially change it) just after an update is made.
20
Here’s the important thing. The value queried from the DOM in
getSnapshotBeforeUpdate will refer to the value just before the DOM is updated.
Even though the render method was previously called.
An analogy that may help has to do with how you use version control systems such
as git.
A basic example is that you write code, and stage your changes before pushing to
the repo.
In this case, assume the render function was called to stage your changes before
actually pushing to the DOM. So, before the actual DOM update, information
retrieved from getSnapshotBeforeUpdate refers to those before the actual visual
DOM update.
Don’t worry if you don’t get it yet. I have an example for you.
21
www.dbooks.org
The implementation of the chat pane is as simple as you may have imagined.
Within the App component is an unordered list with a Chats component:
<ul className="chat-thread">
</ul>
The Chats component renders the list of chats, and for this, it needs a chatList
prop. This is basically an Array. In this case, an array of 3 string values, "Hey",
"Hello", "Hi".
render() {
return (
<React.Fragment>
{this.props.chatList.map((chat, i) => (
{chat}
</li>
))}
</React.Fragment>
);
It just maps through the chatList prop and renders a list item which is in turn
styled to look like a chat bubble :).
There’s one more thing though. Within the chat pane header is an “Add Chat”
button.
22
Clicking this button will add a new chat text, “Hello”, to the list of rendered
messages.
23
www.dbooks.org
The problem here, as with most chat applications is that whenever the number of
chat messages exceeds the available height of the chat window, the expected
behaviour is to auto scroll down the chat pane so that the latest chat message is
visible. That’s not the case now.
24
Let’s see how we may solve this using the getSnapshotBeforeUpdate lifecycle
method.
getSnapshotBeforeUpdate(prevProps, prevState) {
getSnapshotBeforeUpdate(prevProps, prevState) {
25
www.dbooks.org
Whatever value is returned here is then passed on to another lifecycle method.
You’ll get to see what I mean soon.
Let’s call the returned value from getSnapshotBeforeUpdate, snapshot, and here's
what we get thereafter:
Here’s all the code required to maintain the scroll position within the chat pane:
getSnapshotBeforeUpdate(prevProps, prevState) {
return null;
26
Let me explain what’s going on there.
However, the graphic below highlights the actual region that holds the chat
messages (the unordered list, ul which houses the messages).
27
www.dbooks.org
It is this ul we hold a reference to using a React Ref:
...
</ul>
28
First off, because getSnapshotBeforeUpdate may be triggered for updates via any
number of props or even a state update, we wrap to code in a conditional that
checks if there’s indeed a new chat message:
getSnapshotBeforeUpdate(prevProps, prevState) {
getSnapshotBeforeUpdate(prevProps, prevState) {
return null
getSnapshotBeforeUpdate(prevProps, prevState) {
return null;
29
www.dbooks.org
First, consider a situation where the entire height of all chat messages doesn't
exceed the height of the chat pane.
When this is evaluated, it’ll be equal to the scrollHeight of the chat pane — just
before the new message is inserted to the DOM.
If you remember from the previous explanation, the value returned from the
getSnapshotBeforeUpdate method is passed as the third argument to the
componentDidUpdate method. We call this snapshot:
The value passed in here — at this time, is the previous scrollHeight before the
update to the DOM.
In the componentDidUpdate we have the following code, but what does it do?
30
componentDidUpdate(prevProps, prevState, snapshot) {
In actuality, we are programmatically scrolling the pane vertically from the top
down, by a distance equal to chatThreadRef.scrollHeight - snapshot;
Since snapshot refers to the scrollHeight before the update, the above
expression returns the height of the new chat message plus any other related
height owing to the update.
When the entire chat pane height is occupied with messages (and already scrolled
up a bit), the snapshot value returned by the getSnapshotBeforeUpdate method
will be equal to the actual height of the chat pane.
31
www.dbooks.org
The computation from componentDidUpdate will set to scrollTop value to the
sum of the heights of extra messages - exactly what we want.
If you got stuck, I’m sure going through the explanation (one more time) or
checking the source code will help clarify your questions.
Let’s implement a simple component to catch errors in the demo app. For this,
we’ll create a new component called ErrorBoundary.
32
import React, { Component } from 'react';
state = {};
render() {
return null;
static getDerivedStateFromError.
Whenever an error is thrown in a descendant component, this method is called
first, and the error thrown passed as an argument.
Whatever value is returned from this method is used to update the state of the
component.
state = {};
static getDerivedStateFromError(error) {
render() {
return null;
33
www.dbooks.org
}
Right now, whenever an error is thrown in a descendant component, the error will
be logged to the console, console.error(error), and an object is returned from
the getDerivedStateFromError method. This will be used to update the state of
the ErrorBoundary component i.e with hasError: true.
componentDidCatch.
The componentDidCatch method is also called after an error in a descendant
component is thrown. Apart from the error thrown, it is passed one more argument
which represents more information about the error:
componentDidCatch(error, info) {
In this method, you can send the error or info received to an external logging
service. Unlike getDerivedStateFromError, the componentDidCatch allows for
side-effects:
componentDidCatch(error, info) {
static getDerivedStateFromError(error) {
34
console.log(`Error log from getDerivedStateFromError: ${error}`);
componentDidCatch(error, info) {
console.log(info);
render() {
return null
Also, since the ErrorBoundary can only catch errors from descendant
components, we’ll have the component render whatever is passed as Children or
render a default error UI if something went wrong:
...
render() {
if (this.state.hasError) {
return this.props.children;
I have simulated a javascript error whenever you add a 5th chat message. Have a
look at the error boundary at work:
35
www.dbooks.org
Conclusion.
It is worth mentioning that while new additions were made to the component
lifecycle methods, some other methods such as componentWillMount ,
componentWillUpdate, componentWillReceiveProps were deprecated.
36
Now you’re up to date on the changes made to the component lifecycle methods
since React version 16!
37
www.dbooks.org
Click to Tweet.
38
Chapter 2: Simpler State
Management with the
Context API.
John’s an amazing developer, and he loves what he does. However, he’s frequently
been facing the same problem when writing React applications.
39
www.dbooks.org
Props drilling, the term used to describe passing down props needlessly through
a deeply nested component tree, has plagued John for a while now!
Luckily, John has a friend who always has all the answers. Hopefully, she can
suggest a way out.
John reaches out to Mia, and she steps in to offer some advice.
40
Mia is a fabulous engineer as well, and she suggests using some state
management library such as Redux or MobX.
These are great choices, however, for most of John’s use cases, he finds them a
little too bloated for his need.
“Can’t I have something simpler and native to React itself”, says John.
Mia goes on a desperate search to help a friend in need, and she finds the Context
API.
41
www.dbooks.org
Mia recommends using React’s Context API to solve the problem. John is now
happy, excited to see what the Context API could offer, and he goes about his work
productively.
This marks the beginning of John’s experience with the Context API.
Introduction to Context.
The Context API exists to make it easy to share data considered “global” within a
component tree.
Let’s have a look at an illustrated example before we delve into writing any code.
42
Well, John has began working with the Context API and has mostly been
impressed with it so far. Today he has a new project to work on, and he intends to
use the context API.
John is expected to build a game for a new client of his. This game is called Benny
Home Run, and it seems like a great place to use the Context API.
The sole aim of the game is to move Benny from his start position to his new home.
43
www.dbooks.org
To build this game, John must keep track of the position of Benny.
Since Benny’s position is such an integral part of the entire application, it may as
well be tracked via some global application state.
44
Did I just say “global application state”?
Yeah!
45
www.dbooks.org
First, there’s the need to import the createContext method from React
To create a context object, you invoke the createContext method with (or without)
an initial state value to be saved in the context object.
createContext(initialStateValue)
46
x: 50,
y: 50
})
Looks good!
But, after creating a context object, how exactly do you gain access to the state
values within your application?
Well, every context object comes with a Provider and Consumer component.
The Provider component “provides” the value saved in the context object to its
children, while the Consumer component “consumes” the values from within any
child component.
x: 50,
y: 50
})
Since Provider provides value saved in the context object to its children, we
could wrap a tree of components with the Provider component as shown below:
<Provider>
</Provider>
47
www.dbooks.org
Now, any child component within the Root component will have access to the
default values stored in the context object.
<Provider>
<Scene>
<Benny />
</Scene>
</Provider>
Where Scene and Benny are children of the Root component and represent the
game scene and benny character respectively.
In this example, the Scene or the even more nested Benny component will have
access to the value provided by the Provider component.
This value prop is useful if you want to provide a value other than the initial value
passed in at the context object creation time via
createContext(initialStateValue)
Here’s an example where a new set of values are passed in to the Provider
component.
<Scene>
<Benny />
</Scene>
</Provider>
Now that we have values provided by the Provider component, how can a nested
component such as Benny consume this value?
48
The simple answer is by using the Consumer component.
Consider the Benny component being a simple component that renders some
SVG.
Now, within Benny we can go ahead and use the Consumer component like this:
return <Consumer>
</Consumer>
49
www.dbooks.org
}
It is worth noting that whenever the value from a Provider component changes,
the associated Consumer component and the children will be re-rendered to keep
the value(s) consumed in sync.
Also, a Consumer will receive values from the closest Provider above it in the tree.
x: 50,
y: 50
})
<Provider>
<Root />
</Provider>
// do whatever
</Provider>
50
Now, with a new provider component introduced in Benny, any Consumer within
Benny will receive the value {x: 100, y: 100} NOT the initial value of {x: 50,
y: 50}.
This is a contrived illustrated example, but it helps solidify the foundations of using
the Context API.
Having understood the necessary building blocks for using the Context API, let’s
build an application utilizing all you’ve learned so far, with extra use cases
discussed.
One of his many side projects is a bank application he thinks could shape the
future of banking. How so.
I managed to get the source code for this application. You’ll find it in the code
repository for the book as well.
To get started, please Install the dependencies and run the application by
following the commands below:
cd 02-Context-API/bank-app
yarn install
yarn start
Once that’s done, the application starts off with a login screen as seen below:
51
www.dbooks.org
You can enter whatever username and password combination of your choosing.
Upon login in you’ll be presented with the application’s main screen shown below:
In the main screen you can perform actions such as viewing your bank account
balance and making withdrawals as well.
52
Our goal here is to manage the state in this application a lot better by introducing
React’s Context.
The root component of the application is called Root, and has the implementation
below:
...
state = {
loggedInUser: null
evt.preventDefault()
this.setState({
loggedInUser: USER
})
53
www.dbooks.org
render () {
return loggedInUser ? (
) : (
If the user is logged in, the main component App is rendered, else we show the
Login component.
...
loggedInUser ? (
) : (
...
Upon a successful login (which doesn’t require any particular username and
password combinations), the state of the Root application is updated with a
loggedInUser.
...
...
this.setState({
loggedInUser: USER
})
54
...
For this application, I’ve created a fake user in the api directory that exports the
following user object.
name: 'June',
totalAmount: 2500701
Basically, the name and totalAmount of the user’s bank account are retrieved and
set to state when you log in.
Well, consider the main component, App. This is the component responsible for
rendering everything in the app other than the Login screen.
state = {
showBalance: false
displayBalance = () => {
render () {
return (
<div className='App'>
55
www.dbooks.org
<User loggedInUser={loggedInUser} profilePic={photographer} />
<ViewAccountBalance
showBalance={showBalance}
loggedInUser={loggedInUser}
displayBalance={this.displayBalance}
/>
<section>
</section>
<Charity />
</div>
The loggedInUser is passed as props to App from Root, and is also passed down
to both User and ViewAccountBalance components.
56
The User component receives the loggedInUser prop and passes it down to
another child component, Greeting which renders the text, “Welcome, June”.
//User.js
return (
<div>
</div>
57
www.dbooks.org
//ViewAccountBalance.js
return (
<Fragment>
{!showBalance ? (
<div>
<p>
</p>
<button onClick={displayBalance}>
Yes
</button>
</div>
) : (
)}
</Fragment>
58
}
From the code block above, do you also see that ViewAccountBalance receives
the loggedInUser prop only to pass it to TotalAmount?
TotalAmount receives this prop, retrieves the totalAmount from the user object
and renders the total amount.
I’m pretty sure you can figure out whatever else is going on in the code snippets
above.
Having explained the code so far, do you see the obvious props drilling here?
loggedInUser is passed down way too many times to components that don’t even
need to know about it.
One easy solution is to look at the Root component where we began passing
props down and finding a way to introduce a context object in there.
59
www.dbooks.org
Going by that solution, we could create a context object with no initial default
values above the Root class declaration:
state = {
loggedInUser: null
...
Then we could wrap the main App component around the Provider with a value
prop.
state = {
loggedInUser: null
...
render () {
...
return loggedInUser ? (
<Provider value={this.state.loggedInUser}>
</Provider>
...
Initially, the Provider value prop will be null, but as soon as a user logs in and
the state is updated in Root, the Provider will also receive the current
loggedInUser.
60
With this done we can import the Consumer wherever we want and do away with
passing props needlessly down the component tree.
return (
<Consumer>
</Consumer>
We could go ahead and do the same everywhere else we’ve passed the
loggedInUser prop needlessly.
And the app works just as before, only we got rid of passing down loggedInUser
over and over again .
The solution highlighted above works but not without some caveat.
A better solution will be to centralise the logic for the user state and Provider in
one place.
Instead of having the Root component manage the state for loggedInUser, we will
create a new file called UserContext.js.
This file will have the related logic for updating loggedInUser as well as expose a
context Provider and Consumer to make sure loggedInUser and any updater
functions are accessible from anywhere in the component tree.
61
www.dbooks.org
This sort of modularity becomes important when you have many different context
objects. For example, you could have a ThemeContext and LanguageContext
object in the same app.
Extracting these into separate files and components proves more manageable and
effective over time.
// UserContext.js
state = {
loggedInUser: null
evt.preventDefault()
this.setState({
loggedInUser: USER
})
render () {
return (
<Provider
value={{
user: loggedInUser,
handleLogin: this.handleLogin
}}
>
62
{this.props.children}
</Provider>
This represents the content of the new context/UserContext.js file. The logic
previously handled by the Root component as been moved here.
Note how it handles every logic regarding the loggedInUser state value, and
passes the needed values to children via a Provider.
...
<Provider
value={{
user: loggedInUser,
handleLogin: this.handleLogin
}}
>
{this.props.children}
</Provider>
...
In this case, the value prop is an object with the user value, and function to
update it, handleLogin.
Also note that the Provider and Consumer are both exported. This makes it easy to
consume the values from any components in the application.
63
www.dbooks.org
With this decoupled setup, you can use the loggedInUser state value anywhere in
your component tree, and have it updated from anywhere in your component tree
as well.
return (
<UserConsumer>
</UserConsumer>
How easy.
Now, I’ve taken the effort to delete every reference to loggedInUser where the
prop had to be passed down needlessly. Thanks, Context!
For example:
// before
return (
<div>
</div>
64
return (
<div>
<Greeting />
</div>
Be sure to look in the accompanying code folder for the final implementation I.e
after stripping off the loggedInUser from being passed down needlessly.
Well, this app has some buttons. You click them and voila, a withdrawal is made.
65
www.dbooks.org
Since the totalAmount value resides in the loggedInUser object, we may as well
have the logic to make withdrawals in the UserContext.js file.
Remember we’re trying to centralise all logic related to the user object in one
place.
// UserContext.js
...
this.setState({
loggedInUser: {
name,
66
totalAmount: totalAmount - withdrawalAmount
})
When you click any of the buttons, we will invoke this handleWithdrawal method.
This is possible because both buttons have a data-amount attribute set on them.
For example:
Now that we have the handleWithdrawal method written out, we can expose it via
the values prop passed to Provider as shown below:
<Provider
value={{
user: loggedInUser,
handleLogin: this.handleLogin
handleWithdrawal: this.handleWithdrawal
}}
>
{this.props.children}
</Provider>
Now, we’re all set to consume the handleWithdrawal method from anywhere in
the component tree.
In this case, our focus is on the WithdrawButton component. Go ahead and wrap
that in a UserConsumer, deconstruct the handleWithdrawal method and make it
the click handler for the buttons as shown below:
67
www.dbooks.org
const WithdrawButton = ({ amount }) => {
return (
<UserConsumer>
<button
data-amount={amount}
onClick={handleWithdrawal}
>
WITHDRAW {amount}
</button>
)}
</UserConsumer>
68
Conclusion
This illustrates that you can pass not only state values, but also their corresponding
updater functions in a context Provider. These will be available to be consumed
anywhere in your component tree.
Having made the bank app work well with the Context API, I’m pretty sure John will
be proud of the progress we’ve made!
69
www.dbooks.org
Chapter 3: ContextType —
Using Context without a
Consumer.
So far, John has had a great experience with the Context. Thanks for Mia who
recommended such great tool.
As John uses the context API more often, he begins to realise a problem.
70
When you have multiple Consumers within a component, it results to having a lot of
nested, not-so-pleasant code.
Here’s an example.
While working on the Benny Home Run application, John had to create a new
context object to hold the game level state of the current user.
x: 50,
y: 50
})
71
www.dbooks.org
Remember, it’s common practice to split related data into different context objects
for reusability and performance (owing to the fact the every consumer is re-
rendered when values within a Provider change)
With these context objects, John goes ahead to use both Consumer components
within the Benny component as follows.
return <PositionConsumer>
</GameLevelConsumer>}
</PositionConsumer>
Do you notice that consuming values from both context objects result in very
nested code?
Well, this is one of the more common problem with consuming data with the
Consumer component. With multiple consumer components, you begin to have a
lot of nesting.
Firstly, when we learn about Hooks in a later chapter, you’ll come to see an almost
perfect solution to this. In the mean time, let’s consider the solution available to
Class components via something calledcontextType.
72
Using a Class Component with
contextType.
To take advantage of contextType you’re required to work with a class
component.
// Class component
render () {
return <Consumer>
</Consumer>
However, using a Consumer forces you to use a render prop API that may lead to
nested code.
Let’s get rid of the Consumer component by using the contextType class property.
73
www.dbooks.org
Getting this to work is fairly easy.
First, you set the contextType property on the class component to a context
object.
// look here 👇
Benny.contextType = BennyPositionContext
After setting the contextType property, you can go ahead to consume values from
the context object by using this.context.
render () {
74
// look here. No nesting!
Conclusion
The contextType property is does solve the nesting problem a little bit.
However, when we discuss Hooks in a later chapter, you’ll see how much better the
solution hooks offer is.
75
www.dbooks.org
Chapter 4: React.memo ===
Functional PureComponent.
76
However, the only reason he refactored the Benny component to a class
component was because be needed to extend React.PureComponent.
The solution works, but what if we could achieve the same effect without having to
refactor from functional to class components?
Refactoring large components just because you need a specific React feature isn’t
the most pleasant of experiences.
// before
77
www.dbooks.org
function MyComponent ({name}) {
return ( <div>
Hello {name}.
</div>
// after
return ( <div>
Hello {name}.
</div>
})
So, if props do not change, react will skip rendering the component and just use
the previously memoized value.
With this new found information, John refactors the functional component, Benny
to use React.memo.
78
Handling Deeply Nested Props.
React.memo does a props shallow comparison. By implication, if you have nested
props objects, the comparison will fail.
return ( <div>
</div>
}, equalityCheck)
79
www.dbooks.org
If the equalityCheck function returns true, no re-render will happen. This would
mean that the current props and previous props are the same. If you return false,
then a re-render will occur.
If you’re concerned about incurring extra performance hits from doing a deep
comparison, you may use the lodash isEqual utility method.
Conclusion.
React.memo brings the class PureComponent behaviour to functional components.
We’ve also had a look at using the equalityCheck function as well. If you use the
equalityCheck function, be sure to include checks for function props where
applicable. Not doing so is a common source of bugs in many React applications.
80
Chapter 5: The Profiler —
Identifying Performance
Bottlenecks.
It’s Friday and Mia is headed back home. On her way home she can’t help but think
to herself.
81
www.dbooks.org
“What have I achieved today?” Mia says her to herself.
After a long careful thought, she comes to the conclusion that she accomplished
just one thing the entire day.
82
Well, how is it possible that Mia only achieved one thing the entire day?
83
www.dbooks.org
It turns out that all Mia accomplished today was seating helplessly as her browser
attempted to load a page for 7 hours!
84
What???
If you’ve been writing react for sometime, you might remember the react-
addons-perf module.
85
www.dbooks.org
Well, that has been deprecated in favour of the Profiler.
Getting Started.
To keep this as pragmatic as possible, I have set up a tiny application we’re going
to profile together. i.e measure performance. We’ll do this with the aid of the
Profiler.
86
I call the application fake-medium, and it looks like this:
You’ll find the source code for the application in the code repository for this book.
To Install the dependencies and run the app, run the following from the04-The-
Profiler directory:
cd fake-medium
yarn install
yarn start
If you ran those commands, you should have the application running in your
default browser, on port 3000 or similar.
87
www.dbooks.org
Finally, open your chrome devtools by pressing Command+Option+J (Mac) or
Control+Shift+J (Windows, Linux, and Chrome OS).
Then find the React chrome devtools extension tab and click it.
88
You’ll be presented with two tabs, elements and profiler.
You guessed right, our focus is on the profiler tab, so please click it.
89
www.dbooks.org
Doing so will lead you to the following page:
90
How does the Profiler Work?
The Profiler works by recording a session of actual usage of your application. In this
recording session it gathers information about the components in your application
and displays some interesting information you can exploit to find performance
bottlenecks.
After clicking ‘record’, you then go ahead to use your application as you’d expect a
user to.
In this case, I’ve gone ahead to click the medium clap button 3 times!
91
www.dbooks.org
Once you’re done interacting with your application, hit stop to view the information
the Profiler provides.
92
Conceptually, react does work in 2 phases. The render phase where components
are rendered and the virtual DOM diffed, and the commit phase where actual
changes in the virtual DOM are committed to the DOM.
The graphical representation you see on the far right of the profiler represents the
number of commits that were made to the DOM during your interaction with the
app.
The taller the bar is, the longer it took React to render the components in this
commit.
93
www.dbooks.org
In the example above, the Profiler recorded three commits. That make sense since I
clicked the button only 3 times. So, there should be only 3 commits made to the
DOM.
Also the first commit took much longer than the subsequent two commits.
The three bars represent the different commits made to the DOM, and you can
click on any to investigate performance metrics for the particular commit.
First, you have 3 tabs representing different groups of information - each relating
to the selected commit on the right.
94
The flame chart displays information on how long it took your component tree to
render.
You’ll notice that each component in your application tree is represented by bars
of varying lengths and colors.
The length of a bar defines how long it took the component (and its children) to
render.
Judging by the bar length, it appears the Provider component took the longest
time to render. That make sense since the Provider is the main root component of
the app, so the time represented here is the time taken for Provider and all its
children to render.
For example, Provider and a couple other components have a grey color.
95
www.dbooks.org
Well, first we are investing the first commit made to the DOM during the
interaction with the application.
The components with a grey color means they weren’t rendered in this commit. If
the component is greyed out, it wasn’t rendered in this commit. So, the length of
the bar only represents how long it took the component to render previously
before this commit i.e before the interaction with the application.
On careful examination, you’ll see that the only component with a different flame
chart color here is the Clap component.
This component represents the Medium clap button that was clicked.
A yellow bar means the component took the most time to render in this commit.
Well, no other component is coloured which means the Clap button was the only
component re-rendered in this commit.
That’s perfect!
96
You don’t want to click the Clap button and have a different component being re-
rendered. That’ll be a performance hit right there.
In more complex applications, you’ll find flame charts with not just yellow and grey
bars. You’ll find some with blue bars.
What’s worth noting is that, yellow longer bars took more time to render, followed
by the blue ones, and finally grey bars weren’t re-rendered in the particular commit
being viewed.
It’s also possible to click on a particular bar to view more information on why it
rendered or not i.e the props and state passed to the component.
While zoomed in, you can also click the commit bars on top to see the difference in
props or state across each commit render.
97
www.dbooks.org
The Ranked Chart.
Once you understand how the flame chart works, the ranked chart becomes a walk
over.
The ranked chart displays every component that was rendered in the commit being
viewed. It displays this components ranked from top to bottom — with component
which took more time to render at the top.
In this example, we have just the Clap component displayed in the ranked chart
view. That’s okay as we only expect the Clap component to be re-rendered upon
clicking.
You can see how the longer yellow bars are right there at the top, and shorter blue
bars at the bottom. If you look carefully, you’ll notice that the colors fade as you go
98
from top to bottom. From more yellow bars to pale yellow bars, to light blue bars
and finally blue bars.
Component Chart.
Whenever you zoom into a component within the Profiler i.e by clicking its
associated bar, a new option on the right pops up.
Clicking this button will bring you to what’s called the component chart.
The component chart displays how many times a particular component was re-
rendered during the recorded interaction.
In this example, I can click the button to view chart for the Clap component.
This shows three bars representing the three times the Clap component was re-
rendered. If I saw a fourth bar here, I’d begin to get worried as I only clicked three
times and expected only three re-renders.
99
www.dbooks.org
If the selected component didn’t re-render at all, you’ll be presented with the
empty screen below:
NB: you can either view the component chart by clicking the button on the far
right, or by double clicking a component bar from the flame or ranked chart.
Interactions.
There’s one final tab in the profiler, and by default it displays an empty screen:
Interactions let you tag actions performed during a recording session with a string
identifier so they can be monitored and tracked within the profiling results.
The API for this is unstable, but here’s how to enable interactions in your profiling
results.
First install the scheduler module. From your terminal, run yarn add scheduler
within the application directory .
100
Once the installation is done, you need to use unstable_trace function exported
by the module as shown below:
The function is exported as unstable_trace but you can rename it in the import
statement as I have done above.
})
It takes a string identifier, timestamp and a callback function. Ideally, you track
whatever interaction you want by passing it into the callback.
// before
_handleClick () {
// do something
// after
_handleClick () {
// do something
})
The medium clap when clicked calls the _handleClick method. Now, I’ve wrapped
the functionality within the trace method.
101
www.dbooks.org
Clicking three times now record 3 interactions and you can click on any of the
interactions to view more details about them.
The interactions will also show up in the flame and ranked charts.
“I was just profiling the bank application, and it’s so not performant”, she added.
John wasn’t surprised. He had not spent a lot of time thinking about performance,
but with Mia up in his face, he began to have a rethink.
102
“Okay, I’ll have a look and fix whatever bottlenecks I find. Can we do that together?”,
John said while thinking to himself how much help Mia would be since she spotted
the problem anyway.
After spending a couple hours, they found and fixed a coupe of performance
bottlenecks in the application.
In this example, we’ll spin up the bank application and pick up from where we
stopped in the earlier chapter. This time we’ll fix the performance bottlenecks
within the application.
103
www.dbooks.org
Noting the Expected Behaviour.
Let’s consider the bank application we want to profile. The interaction in the bank
app is simple. You click a set of buttons, and the withdrawal amount is updated.
Now, what would you consider the expected behaviour of this app with respect to
re-renders and updates?
The only part of the app visually changing is the withdrawal amount. Before going
into the profiling session, my expectation for a performant application will be that
no unnecessary components are re-rendered, just the component responsible for
updating the total amount.
Give or take, I’ll expect just the TotalAmount component to be re-rendered, or any
other component directly connected with that update.
104
With this expectation set, let’s go on and profile the application.
The steps are the same as discussed earlier. You open your devtools, record an
interaction session, and begin to interpret the results.
Now, I have gone ahead to record a session. In this session, all I did was click the
“withdraw $10,000” button 3 times.
Oh my! From the chart above, so many components were re-rendered. You see the
many bar colours represented in the flame chart ?
105
www.dbooks.org
Interpreting the Flame chart.
First let’s consider what’s likely the root of the problem here. By default, whenever
a Provider has its value changed, every child component is forced to re-render.
That’s how the Consumer gets the latest values from the context object and stays
in sync.
The problem here is that every component apart from Root is a child of Provider
— and they all get re-rendered needlessly!
Some of the child components don’t need to be re-rendered as they are not
directly connected with the change.
The App component doesn’t receive any prop and it only manages the state value
showBalance.
state = {
106
showBalance: false
displayBalance = () => {
render () {
...
It isn’t directly connected with the change, and it’s pointless to re-render this
component.
// before
state = {
showBalance: false
...
// after
state = {
showBalance: false
...
107
www.dbooks.org
Well, have a look at the new flame chart generated after that simple (one-liner)
change.
A lot of App’s children aren’t re-rendered needlessly, and we have a more sane
flame graph now.
Great!
It’s easy to assume that because we had fewer re-renders in the “withdraw amount”
interaction we now have a performant app.
App’s now a PureComponent, but what happens when App gets rendered owing to
a state change?
Well, let’s profile a different interaction. This time, load up the application and
profile the interaction for viewing an account balance.
108
If you go ahead and profile the interaction, we get a completely different result.
109
www.dbooks.org
From the flame graph above, every child component of App as been re-rendered.
They all had nothing to do with this visual update, so those are wasted rendered.
NB: If you need to check the hierarchy of components more clearly, remember you
can always click the elements tab:
Well, since these child components are functional components, let’s use
React.memo to memoize the render results so they don’t change except there’s a
change in props.
// User.js
...
})
// ViewAccountBalance.js
...
})
110
// WithdrawButton.js
...
})
Now, when you do that and re-profile the interaction, we get a much nicer flame
chart:
When you view your flame chart i.e if you’re following along, you may see
something slightly different.
The names of the component may not be shown. You get the generic name Memo
and it becomes difficult to track which component is what.
To change this, set the displayName property for the memoized components.
111
www.dbooks.org
Below’s an example.
// ViewAccountBalance.js
...
})
ViewAccountBalance.displayName = 'ViewAccountBalance'
You go ahead and do this for all the memoized functional components.
We’re pretty much done with resolving the performance leaks in the application,
however, there’s one more thing to do.
The effect isn’t very obvious in this application, but will come handy as you face
more cases in the real world such as in situations where a Provider is nested
within other components.
The implementation of the Provider in the bank application had the following:
...
<Provider
value={{
user: loggedInUser,
handleLogin: this.handleLogin
handleWithdrawal: this.handleWithdrawal
}}
>
{this.props.children}
</Provider>
...
112
The problem here is that we’re passing a new object to the value prop every
single time. A better solution will be to keep a reference to these values via state.
e.g.
<Provider value={this.state}>
{this.props.children}
</Provider>
// context/UserContext.js
constructor () {
super()
this.state = {
user: null,
handleLogin: this.handleLogin,
handleWithdrawal: this.handleWithdrawal
...
render () {
{this.props.children}
</Provider>
Be sure to look in the associated code folder if you need more clarity on this.
113
www.dbooks.org
Conclusion.
Profiling applications and identifying performance leaks is fun and rewarding. I
hope you’ve gained relevant knowledge in this section.
114
Chapter 6: Lazy Loading with
React.Lazy and Suspense.
“Hey John, we need to look into lazy loading some modules in the Benny
application”, says Tunde, John’s manager.
John’s had great feedback from his manager for the past few months. Every now
and then Tunde storms into the office with a new project idea. Today, it’s lazy
loading with React.Lazy and Suspense.
John’s never lazy loaded a module with React.Lazy and Suspense before now. This
is all new to him, so he ventures into a quick study to deliver on what his manager
has requested.
115
www.dbooks.org
What is Lazy Loading?
When you bundle your application, you likely have the entire application bundled
in one large chunk.
To understand lazy loading, here’s the specific use case Tunde had in mind when
he discussed with John.
“Hey John, do you remember the Benny app has an initial home screen?”, said
Tunde.
116
This is the first screen the user encounters when they visit the Benny game. To
begin playing the game, you must click the “Start Game” button to be redirected to
the actual game scene.
“John, the problem here is that we’ve bundled all our React components together
and are all served to the user on this page”.
“Instead of loading the GameScene component and its associated assets, we could
defer the loading of those until the user actually clicks ’Start Game’, huh?”, said
John.
And Tunde agreed with a resounding “Yes, that’s exactly what I mean”.
Lay loading refers to deferring the loading of a particular resource until much later,
usually when a user makes an interaction that demands the resource to be actually
loaded. In some cases it could also mean preloading a resource.
117
www.dbooks.org
Essentially, the user doesn’t get the lazy loaded bundle served to them initially,
rather it is fetched much later at runtime.
React makes lazy loading possible by employing the dynamic import syntax.
Dynamic imports refer to a tc39 syntax proposal for javascript, however, with
transpilers like Babel, we can use this syntax today.
While this is desirable in many cases, the syntax doesn’t allow for dynamically
loading a module at runtime.
For these reasons (and more) there’s a proposal for introducing the dynamic
import syntax to Javascript.
import('path-to-awesome-module')
It has a syntax similar to a function, but is really not a function. It doesn’t inherit
from Funtion.proptotype and you can’t invoke methods such as call and apply.
The returned result from the dynamic import call is a promise which is resolved
with the imported module.
import('path-to-awesome-module')
.then(module => {
})
118
Using React.lazy and Suspense.
React.lazy and Suspense make using dynamic imports in a React application so
easy.
For example, consider the demo code for the Benny application below:
state = {
startGame: false
render () {
return !this.state.startGame ?
<GameInstructions /> :
<Scene />
GameInstructions represents the home page of the game and Scene the entire
scene of the game itself.
119
www.dbooks.org
Consequently, even when the user hasn’t shown intent to start playing the game,
we would have loaded and sent to the user, the complex Scene component which
contains all the logic for the game scene.
// before
// now
React.lazy takes a function that must call a dynamic import. In this case, the
dynamic import call is import('./Scene').
Note that React.lazy expects the dynamically loaded module to have a default
export containing a React component.
With the Scene component now dynamically loaded, when the application is
bundled for production, a separate module (or javascript file) will be created for
the Scene dynamic import.
When the app loads, this javascript file won’t be sent to the user. However, if they
click the “Start Game” button and show intent to load the Scene component, a
network request would be made to fetch the resource from the remote server.
Now, fetching from the server introduces some latency. To handle this, wrap the
Scene component in a Suspense component to show a fallback for when the
resource is being fetched.
120
state = {
startGame: false
render () {
return !this.state.startGame ?
<GameInstructions /> :
// look here
<Scene />
</Suspense>
Now, when the network request is initiated to fetch the Scene resource, we’ll show
a “loading…” fallback courtesy the Suspense component.
Suspense takes a fallback prop which can be a markup as shown here, or a full
blown React component e.g. a custom loader.
With React.lazy and Suspense you can suspend the fetching of a component
until much later, and show a fallback for when the resource is being fetched.
How convenient.
Also, you can place the Suspense component anywhere above the lazy loaded
component. In this case the Scene component.
If you also had multiple lazy loaded components, you could wrap them in a single
Suspense component or multiple, depending on your specific use case.
Handling Errors.
In the real-world, things break often, right?
121
www.dbooks.org
It’s possible that in the process of fetching the lazy loaded resource, a network
error occurs.
To handle such case, be sure to wrap your lazy loaded components in a Error
Boundary.
Here’s an example:
state = {
startGame: false
render () {
return <MyErrorBoundary>
{!this.state.startGame ?
<GameInstructions /> :
<Scene />
</Suspense>}
</MyErrorBoundary>
Now, if an error occurs while fetching the lazy loaded resource, it’ll be graciously
handled by the error boundary.
122
No named exports.
If you remember from the section above, I did mention that React.lazy expects
the dynamic import statement to include a module with a default export being a
React component.
At the moment, React.lazy doesn’t support named exports. That’s not entirely a
bad thing, as it keeps tree shaking working so you don’t import actual unused
modules.
// MyAwesomeComponents.js
Now, if you attempt to use React.lazy with a dynamic import of the module
above, you’ll get an error.
// SomeWhereElse.js
A workaround would be to create some other module that exports one of the
components as a default.
For example, if I was interested in lazy loading the AwesomeA component from the
MyAwesomeComponents.js module, I could create a new module like this:
// AwesomeA.js
// SomeWhereElse.js
123
www.dbooks.org
const AwesomeA = React.lazy(() => import('AwesomeA'))
Problem solved!
We had looked at component based code splitting in the earlier examples, but
another common approach is with route based code splitting.
In this method, the code is split into chunks based on the routes in the application.
We could also take our knowledge of lazy loading one step further by code
splitting routes.
Consider a typical React app that uses react-router for route matching.
<Router>
124
<Switch>
</Switch>
</Router>
);
We could lazy load the Home and About components so they are only fetched when
the user hits the associated routes.
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
</Switch>
</Suspense>
</Router>
);
Easy, huh?
We’ve discussed how React.Lazy and Suspense works, but under the hood, the
actual code splitting i.e generating separate bundles for different modules is done
by a bundler e.g. Webpack.
If you use create-react-app, Gatsby or Next.js then you have this already set up
for you.
125
www.dbooks.org
Setting this up yourself is also easy, you just need to tweak your Webpack config a
little bit.
The official Webpack documentation has an entire guide on this. The guide may be
worth checking if you’re handling the budding configurations in your application
yourself.
<UserProvider>
<UserConsumer>
</UserConsumer>
</UserProvider>
When a user isn’t logged in we display the login page, and the App component
only when the user is logged in.
This is very easy. You use the dynamic import syntax with React.lazy and wrap the
App component in a Suspense component.
Here’s how:
...
126
const App = React.lazy(() => import('./containers/App'))
...
<Suspense fallback='loading...'>
<App />
</Suspense>
Now, if you throttle your network connection to simulate Slow 3G, you’ll see the
intermediate “loading…” text after logging in.
Conclusion.
React.lazy and Suspense are great, and so intuitive to work with, however, they
do not support server side rendering yet.
127
www.dbooks.org
It’s likely this will change in the future, but in the mean time, if you care about SSR,
using react-loadable is your best bet for lazy loading React components.
128
Chapter 7: Hooks — Building
Simpler React Apps.
For the past 3 years John’s been writing React apps, functional components have
mostly been dumb.
129
www.dbooks.org
If you wanted local state or some other complex side effects, you had to reach out
to class component. You either painfully refactor your functional components to
class components or nothing else.
It’s a bright Thursday afternoon, and while having lunch, Mia introduces John to
Hooks.
130
She speaks so enthusiastically, it piques John’s interest.
Of all the things Mia said, one thing struck John. “With hooks, functional
components become just as powerful (if not more powerful) than your typical class
components”, said Mia.
131
www.dbooks.org
That’s a bold statement from Mia.
Introducing Hooks.
Early this year, 2019, the React team released a new addition, hooks, to React in
version 16.8.0.
If React were a big bowl of candies, then hooks are the latest additions, very chewy
candies with great taste!
So, what exactly do hooks mean? And why are they worth your time?
132
One of the main reasons hooks were added to React is to offer a more powerful
and expressive way to write (and share) functionality between components.
In the longer term, we expect Hooks to be the primary way people write
React components — React Team
If hooks are going to be that important, why not learn about them in a fun way!
133
www.dbooks.org
The bowl of candy has been incredibly helpful to people around the world.
The people who made this bowl of candy realized that some of the candies in the
bowl weren’t doing people much good.
A couple of the candies tasted great, yes! But they brought about some complexity
when people ate them — think render props and higher order components?
134
So, what did they do?
135
www.dbooks.org
They did the right thing — not throwing away all the previous candies, but making
new sets of candies.
136
These candies exist for one purpose: to make it easier for you to do the things
you are already doing.
137
www.dbooks.org
These candies aren’t super special. In fact, as you begin to eat them you’ll realize
they taste familiar — they are just Javascript functions!
138
As with all good candies, these 10 new candies all have their unique names.
Though they are collectively called hooks.
Their names always begin with the three letter word, use … e.g. useState,
useEffect etc.
Just like chocolate, these 10 candies all share some of the same ingredients.
Knowing how one tastes, helps you relate to the other.
139
www.dbooks.org
The State Hook.
As stated earlier, hooks are functions. Officially, there are 10 of them. 10 new
functions that exist to make writing and sharing functionalities in your components
a lot more expressive.
For a long time, you couldn’t use the local state in a functional component. Well,
not until hooks.
With useState, your functional component can have (and update) local state.
How interesting.
state = {
count: 1
140
handleClick = () => {
render() {
return (
<div>
<h3 className="center">
</h3>
{count}
</button>
</div>
);
Simple, huh?
Let me ask you one simple question. Why exactly do we have this component as a
Class component?
Well, the answer is simply because we need to keep track of some local state
within the component.
function CounterHooks() {
setCount(count + 1);
141
www.dbooks.org
}
return (
<div>
<h3 className="center">
</h3>
<button
className="center-block"
onClick={handleClick}>
{count}
</button>
</div>
);
What’s different?
A functional component doesn’t have all the Class extend ... syntax.
function CounterHooks() {
function CounterHooks() {
return (
<div>
<button
className="center-block"
</div>
142
);
function CounterHooks() {
return (
<div>
<button
className="center-block"
</div>
);
Before the refactor, the count variable came from the class component’s state
object.
In functional components, and with hooks, that comes from invoking the useState
function or hook.
useState is called with one argument, the initial state value e.g. useState(0) where
0 represents the initial state value to be kept track of.
143
www.dbooks.org
useState(0)
The first value being the current state value being tracked, and second, a function
to update the state value.
Think of this as some state and setState replica - however, they aren’t quite the
same.
function CounterHooks() {
// 🦄
setCount(count + 1)
return (
<div>
<button
className="center-block"
</div>
);
There are a few things to note here, apart from the obvious simplicity of the code!
One, since invoking useState returns an array of values, the values could be easily
destructed into separate values as shown below:
Also, note how the handleClick function in the refactored code doesn’t need any
reference to prevState or anything like that.
144
It just calls setCount with the new value, count + 1.
Simple as it sounds, you've built your very first component using hooks. I know it's
a contrived example, but that's a good start!
NB: it's also possible to pass a function to the state updater function. This is usually
recommended as with setState when a state update depends on a previous value
of state e.g. setCount(prevCount => prevCount + 1)
With class components, we all got used to setting state values in an object whether
they contained a single property or more.
// single property
state = {
count: 0
// multiple properties
state = {
count: 0,
time: '07:00'
In the example above, we only called useState with the actual initial value. Not an
object to hold the value.
useState(0)
145
www.dbooks.org
Consider the component below. It’s the same counter application with a twist. This
time the counter keeps track of the time of click.
function CounterHooks() {
setCount(count + 1);
setTime(new Date())
return (
<div>
<button onClick={handleClick}>{count}</button>
<p>
at: {`${time.getHours()}
...
</p>
</div>
);
146
}
As you can see, the hooks usage is quite the same, except for having a new
useState call.
Now, the timestate variable is used in the rendered markup to display the hour,
minute and second of the click.
<p>
</p>
Great!
Absolutely!
If you choose to do this, you should note that unlike setState calls, the values
passed into useState replaces the state value.
setState merges object properties but useState replaces the entire value.
setState({age: "unknown"})
useState({age: "unknown"})
147
www.dbooks.org
// {age: "unknown"} - initial object is replaced
These side effects may be called “effects” for short, and the effect hook, useEffect
was created for this purpose.
How’s it used?
Well, the useEffect hook is called by passing it a function within which you can
perform your side effects.
useEffect(() => {
})
To useEffect I’ve passed an anonymous function with some side effect called
within it.
The next logical question is, when is the useEffect function invoked?
Well, remember that in class components you had lifecycle methods such as
componentDidMount and componentDidUpdate.
Since functional components don’t have these lifecycle methods, useEffect kinda
takes their place.
Thus, in the example above, the function within useEffect also known as the effect
function, will be invoked when the functional component mounts
(componentDidMount) and when the component updates componentDidUpdate).
148
Here’s that in action.
By adding the useEffect call above to the counter app, we indeed get the log
from the useEffect function.
149
www.dbooks.org
function CounterHooks() {
// 🐢 look here.
useEffect(() => {
}, [count])
setCount(count + 1);
setTime(new Date())
return (
...
);
It’s interesting that the effect function is invoked every time there’s an update.
That’s great, but it’s not always the desired functionality.
What if you only want to run the effect function only when the component mounts?
That’s a common use case and useEffect takes a second parameter, an array of
dependencies to handle this.
150
If you pass in an empty array, the effect function is run only on mount — subsequent
re-renders don’t trigger the effect function.
useEffect(() => {
}, [])
If you pass any values into this array, then the effect function will be run on mount,
and anytime the values passed are updated. i.e if any of the values are changed,
the effected call will re-run.
useEffect(() => {
}, [count])
The effect function will be run on mount, and whenever the count function
changes.
151
www.dbooks.org
What about subscriptions?
It’s common to subscribe and unsubscribe from certain effects in certain apps.
useEffect(() => {
window.addEventListener('click', clicked);
}, [])
In the effect above, upon mounting, a click event listener is attached to the window.
152
If you return a function within your effect function, it will be invoked when the
component unmounts. This is the perfect place to cancel subscriptions as shown
below:
useEffect(() => {
window.addEventListener('click', clicked);
return () => {
window.removeEventListener('click', clicked)
}, [])
There’s a lot more you can do with the useEffect hook such as making API calls.
However, React also provides a way for you to make your own unique candies —
called custom hooks.
A custom hook is just a regular function. However, its name must begin with the
word, use and if needed, it may call any of the React hooks within itself.
Below’s an example:
useMedium(() => {
153
www.dbooks.org
useEffect(() => {
fetch(URI)
},[])
})
- Only Call Hooks at the Top Level i.e. not within conditionals, loops or
nested functions.
- Only Call Hooks from React Functions i.e. Functional Components and
Custom Hooks.
This ESLint plugin is great to ensure you adhere to these rules within your projects.
Advanced Hooks
We have only considered two out of 10 of the hooks React provides!
What’s interesting is that the knowledge of how useState and useEffect works
helps you quickly learn the other hooks.
Curious to learn about those, I have created a hooks cheatsheet with live editable
examples.
154
Why this is important is that you can immediately begin to tinker with real
examples that’ll reinforce your knowledge of how hooks work. All of them!
Remember that learning is reinforced when you actual solve problems and build
stuff.
What’s more interesting as well is, after you get through the live examples for each
of the hooks, there’s an extra section for other generic examples that don’t exactly
fit one hook or require a separate case study.
In the example section you’ll find examples such as fetching data from a remote
server using hooks and more.
155
www.dbooks.org
Chapter 8: Advanced React
Patterns with Hooks
With the release of hooks, certain React patterns have gone out of favour. They can
still used, but for most use cases you’re likely better off using hooks. For example,
choose hooks over render props or higher order components.
There may be specific use cases where these could still be used, but most of the
times, choose hooks.
That being said, we will now consider some more advanced React patterns
implemented with hooks.
Ready?
156
Introduction
This chapter may be the longest in the book, and for good reason. Hooks are likely
the way we’ll be writing React components in the next couple of years, and so they
are quite important.
- Compound Components
- Props Collection
- Prop Getters
- State Initializers
- State Reducer
- Control Props
If you’re completely new to these advanced patterns, don’t worry, I’ll explain them
in detail. If you’re familiar with how these patterns work from previous experiences
with class components, I’ll show you how to use these patterns with hooks.
ReactCorp is beginning to scale their workforce. A lot of engineers are being hired
and John’s beginning to work on building reusable components for the entire
team of engineers.
157
www.dbooks.org
Yes, John can build components with his current React skills, however, with
building highly reusable components comes specific problems.
There’s a million different ways the components can be consumed, and you want
to give consumers of the component as much flexibility as possible.
They must be able to extend the functionality and styles of the component as they
deem fit.
The advanced patterns we’ll consider here are tested and tried methods for
building very reusable components that don’t cripple flexibility.
I didn’t create these advanced patterns. Truth be told, most of the advanced React
patterns were made popular by one guy, Kent Dodds — an amazing Javascript
engineer.
The community has received these patterns extremely well, and I’m here to help
you understand how they work!
158
Compound Components Pattern
The first pattern we will consider is called the Compound Components pattern. I
know it sounds fancy, so I’ll explain what it really means.
159
www.dbooks.org
The main component is usually called the parent, and the separate composed
components, children.
<select>
<option value="value0">key0</option>
<option value="value1">key1</option>
<option value="value2">key2</option>
</select>
With select being the parent, and the many option elements, children.
This works like a compound component. For one, it really makes no sense to use
the <option>key0</option> element without a select parent tag. The overall
behaviour of a select element also relies on having these composed option
elements as well.
Also, the state of the entire component is managed by select with all child
elements dependent on that state.
It is also worth mentioning that compound components are just one of many ways
to express the API for your components.
For example, while it doesn’t look as good, the select element could have been
designed to work like this:
<select options="key:value;anotherKey:anotherValue"></select>
This is definitely not the best way to express this API as It makes passing attributes
to the child components almost impossible.
With that in mind, let’s take a look at an example that’ll help you understand and
build your own compound components.
160
Example: Building an Expandable
Component.
We’ll be building an Expandable component. Did you just ask what that means?
161
www.dbooks.org
Designing the API
It’s usually a good idea to write out what the exposed API of your component
would look like before building it out.
<Expandable>
<Expandable.Icon/>
</Expandable>
In the code block above you’d have noticed I have expressions like this:
Expandable.Header
162
You could as well do this:
<Expandable>
<Icon/>
</Expandable>
The Expandable component being the parent component will keep track of state,
and It will do this via a boolean variable called expanded.
// state
Remember that the children are dependent on the parent compound component
for state.
163
www.dbooks.org
We need to create a context object to hold the component state, and expose the
expanded property via the Provider component. Alongside the expanded
property, we will also expose a function callback to toggle the value of this
expanded state property.
If that sounds alright to you, here’s the starting point for the Expandable
component.
// Expandable.js
return <Provider>{children}</Provider>
164
A context object is created and the Provider component deconstructed. Then we
go on to create the Expandable component which renders the Provider and any
children.
Got that?
With the basic setup out of the way, let’s do a little more.
The context object was created with no initial value. However, we need the
Provider to expose the state value expanded and a toggle function to update the
state.
// Expandable.js
...
// look here 👇
return <Provider>{children}</Provider>
With the expanded state variable created, let’s create the toggle updater function
to toggle the value of expanded — whether true or false.
// Expandable.js
...
// look here 👇
return <Provider>{children}</Provider>
165
www.dbooks.org
The toggle function invokes setExpanded, the actual updater function returned
from the useState call.
Every updater function from the useState call can receive a function argument.
This is similar to how you pass a function to setState e.g. setState(prevState
=> !prevState.value).
This is the same thing I’ve done above. The function passed to setExpanded
receives the previous value of expanded and returns the opposite of that, !
expanded
...
// look here 👇
[]
return <Provider>{children}</Provider>
Not sure how useCallback works? You probably skipped the previous advanced
hooks section that pointed to the cheatsheet. Have a look.
Once we have both expanded and toggle created, we can expose these via the
Provider’s value prop.
...
166
const toggle = useCallback(
[]
// look here 👇
// and here 👇
This works, but the value reference will be different on every re-render causing the
Provider to re-render its children.
...
...
// look here 👇
[expanded, toggle]
useMemo takes a callback that returns the object value { expanded, toggle } and
we pass an array dependency [expanded, toggle] so that the memoized value
remains the same unless those change.
Now, there’s just one other thing to do in the Expandable parent component.
167
www.dbooks.org
Handling State Change Callbacks
Let’s borrow a concept from years of experience with React’s class components. If
you remember, it’s possible to do this with class components:
this.setState({
name: "value"
}, () => {
this.props.onStateChange(this.state.name)
})
If you don’t have experience with class components, this is how you trigger a
callback after a state change in class components.
this.props.onStateChange(this.state.name)
This is good practice when creating reusable components, because this way the
consumer of your component can attach any custom logic to be run after a state
update.
For example:
<Expandable onExpanded={doSomethingPersonal}>
...
</Expandable>
In this example, we assume that after the Expandable component’s expanded state
property is toggled, the onExpanded prop will be invoked — hence calling the
user’s callback, doSomethingPersonal.
168
We will add this functionality to the Expanded component.
Whenever you want to perform a side effect within a functional component, for
most cases, always reach out for useEffect.
useEffect(() => {
props.onExpanded(expanded)
}, [expanded])
The problem however with this is that the useEffect effect function is called at
least once — when the component is initially mounted.
So, even though there’s a dependency array, [expanded], the callback will also be
invoked when the component mounts!
useEffect(() => {
})
The functionality we seek requires that the callback passed by the user isn’t
invoked on mount.
//faulty solution
...
useEffect(
() => {
if(!componentJustMounted) {
props.onExpand(expanded)
169
www.dbooks.org
componentJustMounted = false
},
[expanded]
...
Loosely speaking, the thinking behind the code is correct. You keep track of a
certain variable componentJustMounted and set it to true, and only call the user
callback onExpand when componentJustMounted is false.
Finally, the componentJustMounted value is only set to false after the user
callback has been invoked at least once.
Looks good.
However, the problem with this is that whenever the function component re-
renders owing to a state or prop change, the componentJustMounted value will
always be reset to true. Thus, the user callback onExpand will never be invoked as
it is only invoked when componentJustMounted is falsey.
...
if (!componentJustMounted) {
onExpand(expanded)
...
Well, the solution to this is simple. We can use the useRef hook to ensure that a
value stays the same all through lifetime of the component.
//correct implementation
useEffect(
170
() => {
if (!componentJustMounted.current) {
onExpand(expanded)
componentJustMounted.current = false
},
[expanded]
useRef returns a ref object, and the value stored in the object may be retrieved
from the current property, ref.current
After invoking the user callback, we then update this value to false.
componentJustMounted.current = false
Now, whenever there’s a state or prop change the value in the ref object isn’t
tampered with. It remains the same.
With the current implementation, whenever the expanded state value is toggled,
the user callback function onExpanded will be invoked with the current value of
expanded.
Here’s what the final implementation of the Expandable component now looks
like:
// Expandable.js
171
www.dbooks.org
() => setExpanded(prevExpanded => !prevExpanded),
[]
useEffect(
() => {
if (!componentJustMounted) {
onExpand(expanded)
componentJustMounted.current = false
},
[expanded]
[expanded, toggle]
return (
<Provider value={value}>
{children}
</Provider>
If you’ve followed along, that’s great. We’ve sorted out the most complex
component in the bunch. Now, let’s build the child components.
172
These child components need to consume values from the context object created
in Expandable.js.
Now, we may use the useContext hook to consume the values from the Provider.
//Header.js
173
www.dbooks.org
export default Header
Simple, huh?
It renders a div whose onClick callback is the toggle function for toggling the
expanded state within the Expandable parent component.
// Body.js
The expanded value is retrieved from the context object and used within the
rendered markup. It reads like this: If expanded, render children else render
nothing.
// Icon.js
174
export default Icon
With all child components built, we can set the child components as Expandable
properties. See below:
...
Expandable.Header = Header
Expandable.Body = Body
Expandable.Icon = Icon
<Expandable>
<Expandable.Header>React hooks</Expandable.Header>
<Expandable.Icon />
</Expandable>
Does it work?
You bet!
175
www.dbooks.org
And when expanded:
This works but it has to be the ugliest component I’ve ever seen. We can do better.
While there’s a number of ways to style a React component, and I’m sure you have
a favourite, when you build reusable components it’s always a good idea to expose
a frictionless API for overriding default styles.
Usually, I recommend letting it possible to have your components stylable via both
style and className props.
For example:
// and this.
176
Now, our goal isn’t just styling the component, but to make it as reusable as
possible. This means letting whoever consumes the component style the
component whichever they want i.e using inline styles via the style prop, or by
passing some className prop.
// before
First, let’s change the rendered markup to a button. It’s a more accessible and
semantic alternative to the div used earlier.
// look here 👇
We will now write some default styles for the Header component in a Header.css
file.
// Header.css
.Expandable-trigger {
background: none;
display: block;
font-size: 1rem;
font-weight: normal;
margin: 0;
position: relative;
177
www.dbooks.org
text-align: left;
width: 100%;
outline: none;
text-align: center;
.Expandable-trigger:focus,
.Expandable-trigger:hover {
I’m sure you can figure out the simple CSS above. If not, don’t stress it. What’s
important is to note the default classNameused here, .Expandable-trigger
To apply these styles, we need to import the CSS file and apply the appropriate
className prop to the rendered button.
...
import './Header.css'
className="Expandable-trigger">
{children}
</button>
This works great, however the className is set to the default string Expandable-
trigger.
This will apply the styling we’ve written in the CSS file, but it doesn’t take into the
account any className prop passed in by the user.
It’s important to accommodate passing this className prop as a user might like to
change the default style you’ve set in your CSS.
178
Here’s one way to do this:
// Header.js
import './Header.css'
// look here 👇
return (
{children}
</button>
To prevent this, be sure to pass a default className by using the ES6 default
parameters syntax as shown below:
...
Having provided a default value, if the user still doesn’t enter a className, the
combinedClassName value will be equal to "Expandable-trigger ".
179
www.dbooks.org
Note the empty string appended to the Expandable-trigger. This is owing to
how template literals work.
This solution handles the previously discussed edge cases. If you also want to
explicit about removing null, undefined or any other falsey values, you can do the
following:
I’ll stick with the simpler alternative, and providing a default for className via
default parameters.
With that being said, here’s the final implementation for Header:
// after
...
import './Header.css'
return (
{children}
</button>
So far, so good.
Incase you were wondering, combinedClassName returns a string. Since strings are
compared by value, there’s no need to memoize this value with useMemo.
180
So far, we’ve graciously handled the className prop. How about the option to
override default styles by passing a style prop?
Instead of explicitly destructuring the style prop, we can pass on any other prop
passed by the user to the button component.
return (
<button {...otherProps}>
{children}
</button>
With this done, the Header component receives our default styles, yet allows for
change via the className or style props.
<Expandable.Header className="my-class">
React hooks
</Expandable.Header>
React hooks
</Expandable.Header>
Now, I’ll go ahead and do the same for the other child components, Body and
Icon.
181
www.dbooks.org
// before
// after
import './Body.css'
return expanded ? (
{children}
</div>
) : null
// Body.css
.Expandable-panel {
margin: 0;
min-height: 150px;
// before
182
return expanded ? '-' : '+'
// after
...
import './Icon.css'
...
return (
</span>
// Icon.css
.Expandable-icon {
position: absolute;
top: 16px;
right: 10px;
import './Expandable.css'
...
return (
183
www.dbooks.org
<Provider value={value}>
{children}
</div>
</Provider>
// Expandable.css
.Expandable {
position: relative;
width: 350px;
184
We’ve not just made it beautiful, but it’s customisable as well.
185
www.dbooks.org
And this didn’t take a lot of code.
<Expandable>
<Expandable.Header>Reintroducing React</Expandable.Header>
<Expandable.Icon />
<Expandable.Body>
<img
src='https://i.imgur.com/qpj4Y7N.png'
186
/>
<a
href='https://leanpub.com/reintroducing-react'
target='_blank'
rel='noopener noreferrer'
>
Go get it now.
</a>
</p>
</Expandable.Body>
</Expandable>
You can go one step further to test if overriding styles via the style prop works as
well.
<Expandable>
<Expandable.Header
// look here 👇
Reintroducing React
</Expandable.Header>
...
</Expandable>
187
www.dbooks.org
Yay! it works as expected.
Control Props
The compound component we built in the last section works great, however, it’s
quite limited in its extensibility.
Assume another developer, which we’ll refer to as “user”, decided to use the
component we’ve built, but in a much different way.
188
This user has found some very good information they think is worth sharing. So
within their app, they store this information in an array:
// user's app
const information = [
},
note:
},
In their App, they loop over the list of information and render our Expandable
component with its children as seen below:
// user's app
function App () {
return (
<div className='App'>
<Expandable key={index}>
<Expandable.Header
{header}
</Expandable.Header>
189
www.dbooks.org
<Expandable.Icon />
<Expandable.Body>{note}</Expandable.Body>
</Expandable>
))}
</div>
Below’s the result the user gets when no header has been clicked.
Looks good!
When the user clicks any of the headers, the content expands as designed. In the
graphic below, you can see the result of clicking just one of the headers.
190
What happens when another header is clicked?
Well, the header expands as well. This is exactly how the internals of our
Expandable component works.
As you can see from the graphic above, both clicked headers expand their content.
Sadly, this is not the behaviour the user seeks.
191
www.dbooks.org
What the user really wants is a behaviour similar to an accordion — an accordion
only expand one container at a time. Never expand more than one. If the user clicks
another header, open that and but close the rest.
So, how do extend the Expandable component to handle this use case?
Well, this is where the control props pattern comes into play. Note that I’m using
the compound component we built as an example, in actuality, you can implement
this functionality in any component you wish.
<input />
A controlled input component has its value and onChange handler managed
externally.
This is exactly what we aim for when implementing the control props pattern. We
want the state managed internally to be manageable from the outside via props!
The implementation is quite simple, but I need you to understand how it works
before we delve into writing some code.
<Expandable>
<Expandable.Header>
{someHeader}
</Expandable.Header>
<Expandable.Icon />
192
<Expandable.Body>{someContent}</Expandable.Body>
</Expandable>
Within the Expandable component, the expanded value is internally managed and
communicated to the children.
For example:
<Expandable shouldExpand={someUserDefinedBooleanValue}>
<Expandable.Header>
{someHeader}
</Expandable.Header>
<Expandable.Icon />
<Expandable.Body>{someContent}</Expandable.Body>
</Expandable>
Internally, if the shouldExpand prop is passed, we will give up control of the state
update to the user i.e let the expand value return whatever the user defines in the
shouldExpand prop.
First, we will check if the control prop shouldExpand exists. If it does, then the
component is a controlled component i.e expanded managed externally.
// Expandable.js
const Expandable = ({
...otherProps
}) => {
// see check 👇
...
193
www.dbooks.org
}
Not everyone would pass in the control prop. It’s possible that a lot of users just go
for the default use case of letting us handle the state internally. This is why we
check to see if the control prop is defined.
It all begins with the value the Expandable component communicates to the child
elements.
// before
expanded, toggle
])
Before now, we passed along the expanded and toggle values from the internals
of the Expandable component.
// Expanded.js
...
...
Where shouldExpand is the user’s control prop, and expanded our internal state.
Also, remember that a controlled input element is passed the value prop and
onChange prop. A controlled component receives both the state value and the
function handler as well.
194
Before now, we had an internal toggle function.
[]
// Expandable.js
...
...
With these done, here’s the value communicated to the child elements:
...
getState,
getToggle
])
Note how getState and getToggle are both used to get the desired expanded
state and toggle function depending on whether the component is controlled or
not.
We’re pretty much done implementing the control prop pattern here.
When the component wasn’t controlled, the onExpand prop was a user defined
function invoked after every state change. That’s not going to be relevant if the
195
www.dbooks.org
component is controlled. Why? The user handles the toggle function themself
when the component is controlled. They can choose to do anything in the callback
with the already controlled state value too.
Owing to the explanation above, we need to make sure the useEffect call isn’t run
when the component is controlled.
// Expandable.js
...
useEffect(
() => {
// see !isExpandControlled 👇
onExpand(expanded)
componentJustMounted.current = false
},
...
Huh, but I have not shown you how it solves the user’s problem. That’s next.
We reached out to the user and let them know we have implemented a pattern to
cater for their specific use case.
196
Here’s how.
Within the user app, they send a data-index prop to every Header element.
// before
<Expandable.Header />
// now
function App () {
...
...
)}
With this, the returned element from the header gets a data-index property.
// user's app
function App () {
197
www.dbooks.org
return (
<div className='App'>
<Expandable
onExpand={onExpand}
key={index}
>
<Expandable.Header
data-index={index}
>
{header}
</Expandable.Header>
<Expandable.Icon />
<Expandable.Body>{note}</Expandable.Body>
</Expandable>
))}
</div>
Note the shouldExpand controlled prop being the result of index ===
+activeIndex.
activeIndex is a state variable within the user app. Whenever any header is
clicked, the activeIndex is set to the data-index of the element.
This way the user knows which header was just clicked i.e should be active at that
given time.
This explains why they’ve defined the control prop shouldExpand as shown below:
198
shouldExpand={index === +activeIndex} // 👈 the "+" converts the
activeIndex to a number
This is what’s invoked on every click NOT our custom toggle function. When the
user clicks an header, the activeIndex state variable is set to the data-index from
the click target.
With this the user satisfactorily implements their feature. No two contents are
expanded at the same time.
Be sure to check out the full the source code in the control-props directory.
With our controlled props API, we’ve made it relatively for the user to control the
state of the Expanded component. How convenient!
199
www.dbooks.org
Building a Custom Hook
Before we go into the other advanced patterns, it’s important to understand the
default way of sharing functionality with hooks — building a custom hook.
Only by building on this foundation can we take advantage of the other advanced
patterns to be discussed.
So far we’ve built a compound component that works great! Let’s say you were the
author of some open-source library, and you wanted to expose the “expand”
functionality via a custom hook, how would you do this?
First, let’s agree on the name of your open-source (OS) library. Uh, we’ll leave it as
Expandable — same as before.
Now, instead of having the logic for managing the expanded state in a Expandle
component that returns a Provider, we can get rid of most of that.
// Expandable.js
The useExpanded custom hook will now handle the logic for the expanded state
variable, and useEffectAfterMount the logic for invoking a callback only after
mount.
Note that these custom hooks will pretty much use the same logic as before I.e
within the Expandable compound component we had written earlier. The
difference here will be wrapping these in a custom hook.
// useExpanded.js
200
import { useCallback, useMemo, useState } from 'react'
[]
return value
I’m not going to spend time discussing the internal logic of this custom hook. I did
so in the section on compound components.
// useEffectAfterMount.js
useEffect(() => {
if (!componentJustMounted) {
cb()
componentJustMounted.current = false
201
www.dbooks.org
}, deps)
The only difference here is that useEffectAfterMount doesn’t return any value.
Rather it invokes the useEffect hook. To make this as generic as possible, the
custom hook takes in two arguments, the callback to be invoked after mount, and
the array dependencies for which the useEffect function relies upon.
Also, note that line 7 reads, return cb(). This is to make sure to handle
unsubscriptions by returning whatever is returned by the callback — if any e.g a
function to handle unsubscriptions.
Great!
Now that you’ve built these custom hooks, how would a typical consumer of your
OS library use these hooks?
// User's app
function App () {
return (
</div>
The consumer imports your useExpanded custom hook and invokes the function to
retrieve expanded and toggle.
202
With these values, they can render whatever they want. We provide the logic, and
let the user render whatever UI they deem fit. Here’s the result of what this user
chose to do:
And this button invokes the exposed toggle function from the custom hook. Based
on the toggled expanded state, they render the following:
Like the render props API, we’ve given control over to the user to render whatever
UI they want., while we handle whatever logic is required for them to do this.
// User's app
function App () {
// look here 👇
useEffectAfterMount(
() => {
},
203
www.dbooks.org
[expanded]
return (
</div>
Now, whenever the button is clicked, they’ll indeed get the logs!
If you were building such an OS library, as opposed to just exporting these custom
hooks, it may be nice to also export some default UI elements.
204
For convenience, if the user is reaching out for a Expandable library, they need the
custom logic you’ve extracted into a reusable hook, yes, but they also need to
render some UI elements.
You could also export some default Header, Body and Icon elements built to have
their styling configurable.
This sounds like we can reuse the components we built earlier, however we need
to do some refactoring.
The child elements for the compound component required access to some context
object. We may get rid of that and solely base the contract on props.
// Body.js (before)
...
// look here 👇
return ...
const Body = ({
...
return ...
The same may be done for the Icon and Header components.
By providing these default UI elements, you allow the consumer of your library the
option of not having to bother about UIs as much.
205
www.dbooks.org
// user's app
function App () {
return (
<div className='Expandable'>
</div>
It’s important to note that you don’t always have to do this. You can always share
custom functionality by just writing a custom hook.
206
We’ve done a brilliant job building this imaginary OS library, huh? In the following
sections we’ll incorporate even more advanced patterns, and users of the library
will find them incredibly useful.
Props Collection
Regardless of the component you build, and even though it’s going to be used by
different people, some things will remain the same across all users of the
component.
Well, consider the the Expandable component we’ve worked so hard at.
First, regardless of the UI solution the user decides to go for e.g. a custom UI,
they’ll always need an onClick handler for the “toggle element”.
// user 1.
// user 2.
// user 3.
Every user, regardless of the element they choose to render, still needs to handle
the onClick callback.
Also, if these users care about accessibility at all, they also will do this:
// user 1.
207
www.dbooks.org
// user 2.
// user 3.
Depending on your use case there could be more “common props” associated
with your reusable hook or component.
Is there a way we can prevent the users from writing these props every single time?
Can we expose some API from within your reusable custom hook or component?
This is the question the props collection pattern answers with a resounding yes!
[]
// look here 👇
() => ({
onClick: toggle,
'aria-expanded': expanded
}),
208
[toggle, expanded]
...
return value
Note that we’ve called this props collection togglerProps because it is a prop
collection for the toggler i.e the toggle element - regardless of which UI element it
is.
We’ll then expose the togglerProps from the custom hook via the returned value
variable.
...
expanded,
toggle,
togglerProps
])
return value
// user's app
function App () {
209
www.dbooks.org
...
return (
// look here 👇
</div>
This works like before, only the user didn’t have to manually write the “common
props” for the toggle element.
210
In the next section, we will consider an alternative (perhaps more powerful)
solution to the same problem the props collection pattern aims to solve.
Prop Getters
In JavaScript, all functions are objects. However, functions are built to more
customizable and reusable.
211
www.dbooks.org
Consider the following:
If you wanted to created the same object but allow for some level of
customizability, you could do this:
Now, this objCreator function can then be called multiple times to create different
object as shown below:
Well, this is the difference between the props collection pattern and prop getters
pattern.
Owing to the customisability of functions, using a prop getter allows for some
more interesting use cases.
First, let’s start by refactoring the previous solution to use a props getter, then we’ll
consider some interesting use cases.
// before
() => ({
onClick: toggle,
'aria-expanded': expanded
}),
[toggle, expanded]
212
// now
() => ({
onClick: toggle,
'aria-expanded': expanded
}),
[toggle, expanded]
We’ll expose this via the returned value variable within the useExpanded hook as
shown below:
...
() => ({
onClick: toggle,
'aria-expanded': expanded
}),
[toggle, expanded]
// look here 👇
expanded,
toggle,
getTogglerProps
])
return value
213
www.dbooks.org
}
// before
// now
And that’s it! The user’s app works just like before.
I know what you’re thinking. If we’re not going to pass any arguments to the
getTogglerProps function, why bother with the refactor?
That’s a great question, but we’ll pass in some arguments real soon.
With the prop getter, getTogglerProps we can cater for other additional props the
user may be interested in passing down to the toggle element.
If the user needs some more props we haven’t catered for in the prop collection,
they could do this:
</button>
214
However, since these are still part of the “toggler props”, though not common
enough to make them a part of the default props collection, we could allow for the
following usage:
<button
{...getTogglerProps({
id: 'my-btn-id',
})}
>
215
www.dbooks.org
Click to view awesomeness...
</button>
It’s arguably more expressive. So, let’s extend the getTogglerProps function to
handle this use case.
...
({ ...customProps }) => ({
onClick: toggle,
'aria-expanded': expanded,
...customProps
}),
[toggle, expanded]
Since the user will invoke getTogglerProps with an object of custom props, we
could use the rest parameter to hold a reference to all the custom props.
({ ...customProps }) => ({
})
Then we could spread over all custom props into the returned props collection.
({ ...customProps }) => ({
onClick: toggle,
'aria-expanded': expanded,
// look here 👇
...customProps
})
With this extension to getTogglerProps we’ve handled the case for custom props
not catered for in the default props collection we expose.
216
How interesting!
We allowed for passing custom props into the getToggler props getter. Now, what
if the user wanted to pass in a custom onClick prop?
// user's app
...
...
<button
{...getTogglerProps({
id: 'my-btn-id',
// look here 👇
onClick: customClickHandler
})}
>
</button>
If the user does this, it completely overrides the onClick handler within the
useExpanded custom hook and breaks the functionality of the app.
217
www.dbooks.org
The button no longer expands the element, but the user’s custom click handler is
invoked whenever you click the button.
This is because we have an onClick property in the returned object, but this is
overridden by the user’s.
// useExpandable.js
...
218
({ ...customProps }) => ({
'aria-expanded': expanded,
...customProps
}),
[toggle, expanded]
If you move the position of the internal onClick handler, you can override the
user’s.
({ ...customProps }) => ({
'aria-expanded': expanded,
...customProps,
}),
[toggle, expanded]
This works. However, we want a component that’s as flexible as possible. The goal
is to give the user the ability to invoke their own custom onClick handler.
Well, we could have the internal onClick invoke the user’s click handler as well.
Here’s how:
// useExpanded.js
...
'aria-expanded': expanded,
219
www.dbooks.org
onClick: callFunctionsInSequence(toggle, onClick),
...props
}),
[toggle, expanded]
...
Now instead of returning an object with the onClick set to a single function i.e our
internal toggle function or the user’s onClick, we could set the onClick property
to a function, callFunctionsInSequence that invokes both functions - our toggle
function and the user’s onClick!
...
Also, note how we destructure the user’s onClick handler and use the rest
parameter to handle the other props passed in. So you don’t get an error if no
object is passed in by the user, we set a default parameter value of {}.
...
})
Now back to the elephant in the room. How do we write a function that takes one
or two functions and invokes all of them without neglecting the passed arguments?
This function must not break if any of the function arguments is undefined too.
220
fns.forEach(fn => fn && fn(...args))
When we do this:
...
The return value saved in the onClick object property is the returned function
from invoking callFunctionsInSequence:
If you remember from basic React, we always attach click handlers like this:
...
render() {
console.log(evt.target.value)
...
The function receives whatever arguments are passed into the function,
(...args), and invokes all function parameters with these arguments if the
function isn’t falsey. fn && fn(...args).
221
www.dbooks.org
In this case, the argument received by the function will be the event object and it’ll
be passed down to both toggle and onClick as arguments.
The full implementation of the custom hook now looks like this:
// useExpanded.js
[]
'aria-expanded': expanded,
222
onClick: callFunctionsInSequence(toggle, onClick),
...props
}),
[toggle, expanded]
expanded,
toggle,
getTogglerProps
])
return value
With this addition, our internal click handler, toggle and the user’s custom handler,
onClick are both invoked when the toggle button is clicked!
223
www.dbooks.org
How amazing. We wouldn’t be able to cater for this case with just a prop collection
object. Thanks to prop getters, we have a lot more power to provide very reusable
components.
224
State Initializers
To Initialize means to set the value of something. Going by that definition, the state
initializer pattern exists to make it easy for the consumer of your custom hook to
set the “value of state”.
Note that the state initializer pattern doesn’t give full control over setting “value of
state” every single time. It mostly allows for setting initial state and resetting state.
This isn’t the same as full control over setting the state value, but it offers some
benefits as you’ll see soon.
// look here 👇
...
We’ve assumed that the initial state passed to the useState call will be “false”
every single time.
Let’s have this value controllable from an argument the user can pass in. Let’s call
this parameter initialExpanded as seen below:
...
return value
Now the user can pass in initialExpanded into the useExpanded custom hook
call to decide the initial state of the expanded component.
225
www.dbooks.org
Here’s an example where a user may want the terms and condition content
expanded by default:
// user's app
function App () {
return (
...
With our previous setup this was not an easy feat for the user because we had
internally hardcoded the initial state as false .
While we’ve made it easier for the user to dictate the initial state within the custom
hook, they should also be able to reset the state to the initial state at any point in
226
time i.e a reset callback they can invoke to reset the state to the initial default state
they provided.
Assume the terms and condition content in the user app was so long that they
changed the default expanded state to false i.e the expandable content isn’t
open by default.
Since the content was long, they decided to provide a button towards the end of
the write-up so a reader could click to revert the expandable content to the initial
closed state (which may mean close the expandable content and perform some
cleanups).
Even though this particular example isn’t the most realistic one for the reset
functionality, in larger applications you can solve the problem using the same
method discussed here.
All the reset function should do is set the “expanded” state back to the default
provided i.e initialExpanded
() => {
// look here 👇
setExpanded(initialExpanded)
},
[initialExpanded]
...
227
www.dbooks.org
}
The code’s simple enough. All the reset function does is call setExpanded with
the initialExpanded value to reset the state back to the initial state supplied by
the user.
Remember, for this consumer of our custom hook, they want to close the terms and
condition body and also perform some cleanup/side effect.
How will this user perform the cleanup after a reset? We need to cater for this use
case as well.
Let’s get some ideas from the implemented solution for the user to run custom
code after every change to the internal expanded state.
useEffectAfterMount(
() => {
},
[expanded]
Well, now we need to make the same possible after a reset is made. Before I
explain the solution to that, have a look at the usage of useEffectAfterMount in
the code block above.
Now, for the regular state update, the user just had to pass in the array
dependency [expanded] to get the effect function to run after every expanded
state update.
228
For a reset what do we do?
// user's app
useEffectAfterMount(
() => {
},
[resetDep]
...
We need to provide a reset dependency the user can pass into the
useEffectAfterMount array dependency.
Well, the first thing that comes to mind is a state value to be set whenever the user
invokes the reset callback. The state value will keep track of how many times a
reset’s been made.
If we increment the counter variable every time a reset is made, we can expose this
as a reset dependency as it only changes when an actual reset is carried out.
// useExpanded.js
...
() => {
setExpanded(initialExpanded)
229
www.dbooks.org
// increase reset count - call this resetDep
},
[initialExpanded]
...
// useExpanded.js
...
() => ({
expanded,
toggle,
getTogglerProps,
reset,
resetDep
}),
return value
...
// user's app
...
useEffectAfterMount(
() => {
230
// user can perform any reset cleanup/side effect
},
[resetDep]
...
There’s arguably one problem with the solution. Should we really be saving the
reset count as a new state variable?
The useExpanded custom hook is mostly responsible for managing the expanded
state. Introducing a new state variable feels like some inner conflict/pollution.
Before going on, this is just another case of personal preference. There’s nothing
technically wrong with the solution above.
I, on the other hand may prefer to provide the resetDep via a ref object. This way
I’m sure to introduce only important variables as actual state variables.
The solution is similar, it’s only an internal change within our custom hook,
useExpanded. How the user consumes the resetDep remains the same.
// useExpanded.js
231
www.dbooks.org
...
() => {
setExpanded(initialExpanded)
++resetRef.current
},
[initialExpanded]
...
...
resetDep: resetRef.current
And that’s it! Same functionality, different approaches. Feel free to use whichever
feels right to you. I pick the second though :)
As a summary, remember that with the state initialiser pattern you offer the user to
ability to decide initial state within your custom hook, you allow for resetting and
invoking a custom function after a reset is made as well.
Even If you had a more complex custom hook with multiple state values, you could
still use this technique. Perhaps, receive the initial state passed in by the user as an
object. Create a memoized initialState object from the user’s and use this
within your custom hook.
If you also choose to manage your internal state via useReducer, you can still apply
this pattern.
Point is, regardless of your implementation, the goal remains the same. Provide the
user with the ability to decide initial state, allow them to reset and invoke custom
callbacks after a reset is made.
232
In the next section we’ll take things a notch higher as we give even more control to
the users of our components. Remember, that’s what building highly reusable
components is mostly about.
State Reducer
The state reducer pattern is the final and perhaps most advanced pattern we’ll be
discussing. Don’t let that scare you. I’ll take my time to explain how it really works
without leaving behind why it matters as well.
When we write custom hooks or components, they most likely have some way to
keep track of internal state. With the state reducer pattern, we give control to the
user of our custom hook/component to control how state is updated internally.
The technical term for this is called "inversion of control”. In lay man terms it means
a system that allows the user to control how things work internally within your API.
The typical flow for communicating updates to the user of your custom hook or
component looks like this:
233
www.dbooks.org
Within your custom hook you call useState or useReducer or setState to update
state internally. Once the update is already made, the new state value is
communicated to the user.
This is how all the patterns we’ve looked at so far has worked.
With the state reducer pattern, there’s a significant change in how internal state
updates are made.
234
Here’s a graphical illustration:
235
www.dbooks.org
With state reducers, what’s important to note is that before we make any internal
state update, we send our proposed changes over to the user. If the user is okay
with the changes, we go ahead and update state with our proposed changes. If
they need to override a state change, they can do that as well.
Refactoring to useReducer
The most intuitive way to make the state reducer pattern work is using the
useReducer hook to manage internal state.
First, let’s refactor our custom hook to use useReducer instead of useState.
To refactor our custom hook to use the useReducer hook for state updates,
everything within the custom hook stays the same except how the expanded state
is managed.
First, we’ll change the state from just a boolean value, true or false to an object
like this: {expanded: true || false}.
// useExpanded.js
...
// useExpanded.js
// before
...
236
// now
...
Note how we need to destructure expanded from the state object. It’s worth
mentioning that setExpanded returned from the useReducer call now works as a
dispatcher. I’ll show the implication of this soon.
If you’re new to useReducer, it is typically called with a reducer and an initial state
value.
useReducer(internalReducer, initialState)
If you’ve Redux in the past then you’re most likely familiar with the concept of
reducers. If not, a reducer is just a function that receives state and action to
return a new state.
action usually refers to an object, but useReducer isn’t strict about this. If we stick
to the Redux convention, a state update is always made by “dispatching” an action.
The “dispatcher” is the second value returned by the useReducer call. Also, In
order for the reducer to identify each action being dispatched, a type property
always exist on the action.
// for example
action = {
type: "AN_ACTION_TYPE"
I don’t want to turn this into a redux guide, but if you need a refresher, I wrote an
illustrated guide that’ll make the concept of reducers and actions all sink in.
With the useReducer call in place, we need to actually write the reducer used
within the call i.e internalReducer
// useExpanded.js
237
www.dbooks.org
const internalReducer = (state, action) => {
switch (action.type) {
case useExpanded.types.toggleExpand:
return {
...state,
case useExpanded.types.reset:
return {
...state,
default:
...
Remember I said actions typically have a type property. The reducer checks this
type property and returns new state based on the type.
To stay consistent and prevent unwanted typos, the available types for the
useExpanded custom hook are centralised within the same file.
// useExpanded.js
...
useExpanded.types = {
toggleExpand: 'EXPAND',
238
reset: 'RESET'
switch (action.type) {
return {
...state,
expanded: !state.expanded
...
Note that the returned value from the reducer represents the new state of the
expanded state. For example, here’s the returned value for type,
useExpanded.types.toggleExpand.
...
return {
...state,
expanded: !state.expanded
...
For this to come full circle, consider how the reset function now works.
// before
setExpanded(initialExpanded)
// now
...
setExpanded({
type: useExpanded.types.reset,
239
www.dbooks.org
payload: initialExpanded // pass a payload "initialExpanded"
})
Note how the reducer returns new state based on this dispatched reset action:
...
case useExpanded.types.reset:
return {
...state,
...
// useExpanded.js
240
const toggle = useCallback(
[]
...
The available types for state updates are then centralised to prevent string typos.
// useExpanded.js
...
useExpanded.types = {
toggleExpand: 'EXPAND',
reset: 'RESET'
// before
setExpanded(initialExpanded)
// now
...
setExpanded({
type: useExpanded.types.reset,
})
...
241
www.dbooks.org
Please have a look one more time. This will only prove difficult if you don’t know
how useReducer works. If that’s the case, I suggest you quickly check that out.
Having completed this refactor, we need to implement the actual state reducer
pattern.
// user's app
...
Remember from the illustration above that we need to communicate our internal
state changes to the user. The way we’ll do this is by accepting a second
parameter, the user’s own reducer.
For example:
// user's app
function App () {
false,
...
Before we go ahead with the implementation, let’s see how this particular user
intends to use our custom hook.
242
The User’s Application
Our user is an hacker who has found proof of a big conspiracy going on in Mars. To
share this with the world, they want to use our useExpanded hook and default UI
elements we provide to build the following expandable view:
What’s more interesting is that the hacker only wants a reader to view this secret
once. It’s such a big conspiracy.
The hacker’s goal is that whenever the user clicks the button to view the secret, the
expandable content is reset to the default unexpanded state, and the reader can
click all they want on the header but the content won’t be expanded.
You may check the final working version of this user app by running the app in the
state-reducer directory.
Do you see why the user needs the state reducer pattern?
By default, whenever the Header element is clicked, the hacker calls the toggle
function.
...
243
www.dbooks.org
// look here 👇
</Header>
...
Internally, the toggle function always toggles the expanded state property. The
problem is, when the reader has viewed the secret, the hacker wants further clicks
on the Header to not trigger a state update.
In the mean time, here’s how the hacker has handled viewing the secret document.
They attach a click handler to the button which points to the reset callback we
provide.
...
<section className='App'>
...
<Body>
<p>
</p>
</Body>
</div>
</section>
The reset callback resets the expanded state to the initial expanded state of
false provided by the hacker.
...
false)
244
The initial state provided by the hacker is false — this means the expandable
content is closed after the click.
Good.
Within the useEffectAfterMount hook, they then perform a side effect, opening
the secret in a new window tab based on the resetDep which changes when the
user clicks the button.
...
useEffectAfterMount(
() => {
window.open('https://leanpub.com/reintroducing-react', '_blank')
},
[resetDep]
...
So far the hacker is happy with the API we’ve provided. We just need to handle his
last use case for preventing further state updates on clicking the Header element
after a reader has viewed the secret once.
Our solution to the hacker’s feature request is to implement the state reducer
pattern.
When the reader clicks the Header element, we’ll communicate the changes we
propose to be made internally to the hacker i.e we’ll let the hacker know we want
to update the expanded state internally.
245
www.dbooks.org
The hacker will then check their own application state. If the reader has already
viewed the secret resource, they’ll communicate to our custom hook NOT to allow
a state update.
There it is. The power of the state reducer pattern, giving control of internal state
updates to the user.
First, we’ll expect a second argument from the hacker, their own reducer.
...
The second parameter to our useExpanded hook represents the user’s reducer.
After the refactor to useReducer, we got the value of the expanded state by calling
our internal reducer.
...
The problem with this is that our internal reducer always returns our internal
proposed new state. This isn’t the behaviour we want. Before we decide what the
expanded state will be, we need to communicate our proposed state change to the
user i.e the hacker.
246
The implementation of resolveChangesReducer begins by receiving the state and
action, and holding a reference to our internal change.
// look here 👇
...
A reducer always returns new state. The value internalChanges holds the new
state we propose internally by invoking our internalReducer with the current
state and action.
We need to communicate our proposed changes to the user. The way we do this is
by passing this internalChanges to the user’s reducer i.e the second argument to
our custom hook.
// look here 👇
...action,
internalChanges
})
...
Right now, we invoke the user’s reducer with the internal state and an action object.
Note that the action object we send to the user is slightly modified. It contains the
action being dispatched and our internal changes!
... action,
internalChanges: internalChanges
247
www.dbooks.org
}
Every reducer takes in state and action to return a new state. It is the responsibility
of the user’s reducer to return whatever they deem fit as the new state. They can
either return our changes if they’re happy with it OR override whatever changes
they need to.
// useExpanded.js
...
...action,
internalChanges
})
// look here 👇
return userChanges
...
Have a look at the complete implementation of the state reducer. I’ve omitted
whatever hasn’t changed. You can look in the source yourself if you care about the
entire source code (and you should!)
// necessary imports 👇
248
...
switch (action.type) {
case useExpanded.types.toggleExpand:
return {
...state,
expanded: !state.expanded
case useExpanded.types.reset:
return {
...state,
expanded: action.payload
default:
currentInternalState,
action
...action,
internalChanges
})
249
www.dbooks.org
return userChanges
resolveChangesReducer,
initialState
...
() => ({
expanded,
toggle,
getTogglerProps,
reset,
resetDep: resetRef.current
}),
return value
useExpanded.types = {
toggleExpand: 'EXPAND',
reset: 'RESET'
250
With the pattern now implemented internally, here’s how the hacker took
advantage of that to implement their feature.
function App () {
// ref holds boolean value to decide if user has viewed secret or not
false,
if (hasViewedSecret.current) {
return {
...action.internalChanges,
expanded: false
return action.internalChanges
useEffectAfterMount(
251
www.dbooks.org
() => {
window.open('https://leanpub.com/reintroducing-react', '_blank')
hasViewedSecret.current = true
},
[resetDep]
...
I hope you find it good enough to understand how a user may take advantage of
the state reducer pattern you implement in your custom hooks / component.
With this implementation, the hacker’s request is fulfilled! Here’s a GIF that shows
the app in action.
There are many different ways the user could choose to dictate how internal state is
updated.
For example the hacker could be more explicit about interfering state changes
only when the action type refers to a toggle expand.
// user's app
...
if (
hasViewedSecret.current &&
// look here 👇
) {
return {
252
...action.internalChanges,
expanded: false
return action.internalChanges
...
By doing this we may extend our custom hook’s functionality to expose a override
callback like this:
// useExpanded.js
...
// exposed callback 👇
[]
...
useExpanded.types = {
toggleExpand: 'EXPAND',
reset: 'RESET',
The override type is then handled via our internal reducer as follows:
...
case useExpanded.types.override:
return {
...state,
253
www.dbooks.org
expanded: !state.expanded // update state
...
What this allows for is a way to override changes. For example, the hacker may
decide to show a button a user may click in order to view the secret again.
false,
appReducer
...
{ hasViewedSecret.current && (
<button onClick={override}>
</button>
)}
This allows for the expanded container to be open — and letting the use view the
secret one more time. Here’s a GIF showing how that works.
254
Cleanups
Right now, if some user other than this crazy hacker uses our custom function
without passing a reducer, their app breaks.
Let’s provide a default reducer within our custom hook i.e for users who don’t need
this feature.
// userExpanded.js
function useExpanded (
initialExpanded = false,
) {
...
255
www.dbooks.org
Using the ES6 default parameters a default reducer has been provided. The default
user reducer takes in state and action, then returns our internalChanges.
With this fix, users who don’t need the reducer feature don’t get an error.
Remember, the benefit of the state reducer pattern is in the fact that it allows the
user “control” over how internal state updates are made.
Our implementation has made our custom hook so much more reusable. However,
how we update state is now part of the exposed API and if you make changes to
that if may be a breaking change for the user.
Regardless, this is still such a good pattern for complex hooks and components.
Conclusion
This has been a lengthy discourse on Advanced React Patterns with Hooks. If you
don’t get all of it yet, spend a little more time practising these patterns in your day
to day work, and I’m pretty sure you’ll get a hang of it real quick.
When you do, go be the React engineer building highly reusable components with
advanced hook patterns.
256
Last Words
Do have a successful career, and make to practice what you’ve learnt here.
Cheers,
- Ohans Emmanuel
257
www.dbooks.org