Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
261 views

Advanced React Patterns

The document provides an overview of what will be covered in an upcoming React course. It outlines several advanced React patterns that will be discussed, including compound components, prop getters, state initializers, and state reducers. It also explains that students will have an online development environment to test code examples directly in the browser without needing a local setup. The course will cover why component reusability is important and how advanced React patterns can help achieve highly reusable components.

Uploaded by

Daniel Igwe
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
261 views

Advanced React Patterns

The document provides an overview of what will be covered in an upcoming React course. It outlines several advanced React patterns that will be discussed, including compound components, prop getters, state initializers, and state reducers. It also explains that students will have an online development environment to test code examples directly in the browser without needing a local setup. The course will cover why component reusability is important and how advanced React patterns can help achieve highly reusable components.

Uploaded by

Daniel Igwe
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 128

What to Expect

Here's a quick overview of what you'll learn in this course!

WE'LL COVER THE FOLLOWING

• Learning Outcomes
• What We Provide You With

Learning Outcomes #
Hooks are most likely the way we’ll be writing React components for the next
couple of years, so they are quite important to know.

In this course, we’ll consider the following advanced React patterns:

Compound Components
Props Collection
Prop Getters
State Initializers
State Reducers
Control Props

If you’re completely new to these advanced patterns, don’t worry! I’ll explain
them in detail in this course. If you’re familiar with how these patterns work

from previous experiences, I’ll show you how to implement these patterns
with hooks.

What We Provide You With #


In this course, we will provide you with a development environment to test
and run all of the React application code right here in your browser! You don’t
have to go through the hassle of setting up the environment locally but you
can look online for setup guides if you want to set up the environment on
your device.

Now let’s get started!


Why Use Advanced Patterns?`

Advanced patterns with hooks allow reusable React components to be incredibly exible.

WE'LL COVER THE FOLLOWING

• Why is Component Reusability Important?


• Advanced React Patterns to the Rescue!

Why is Component Reusability Important? #


John has had a fairly good career. Today he’s a senior front-end engineer at
ReactCorp; a great startup making the world a better place.

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.
Yes, John can build components with his current React skills. However,
building highly reusable components creates nuanced problems.
There are many different ways components can be consumed, and you want
to be sure to give the consumers as much flexibility as possible.

They must be able to extend the functionality and styles of the component as
they deem fit. This is where advanced patterns come into the picture.

Advanced React Patterns to the Rescue! #


The advanced patterns we’ll consider here are tried and true methods for
building reusable components that don’t sacrifice flexibility.

I didn’t create these advanced patterns. In fact, most of the advanced React
patterns were made popular by one man, 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!

Let’s start with compound components in the next chapter.


What are Compound Components?

Compound components in React are components that are composed of two or more separate components.

WE'LL COVER THE FOLLOWING

• Introduction
• Example: select
• Quick quiz!

Introduction #
The first pattern we will consider is called the Compound Components
pattern. I know it sounds fancy, but I’ll break it down.

The keyword in the pattern’s name is the word Compound.

Literally, the word compound refers to something that is composed of two or


more separate elements.

With respect to React components, this could mean a component that is


composed of two or more separate components.

It doesn’t end there though.

Any React component can be composed of 2 or more separate components. So,


that’s really not a good way to describe compound components.

With compound components, there’s more. The separate components within


which the main component is composed cannot be used without the parent.

The main component is usually called the parent, and the separate composed
components, children.
But if the children components are used without the outer parent components,
1 of 3

But if the children components are used without the outer parent components,
2 of 3
x

They will not make sense and an error will be generated


3 of 3

Example: select #
A classic example is the HTML select element.

<select>
<option value="value0">key0</option>
<option value="value1">key1</option>
<option value="value2">key2</option>
</select>

An HTML example of a compound component

Here, the select element is the parent, and the option elements are children

This works like a compound component. For one, it no sense to use the
<option>key0</option> element without a select parent tag. The overall
behavior of a select element also relies on having these composed option
elements as well. Hence, they are connected to one another.

The state of the entire component is managed by select with all child
e state o t e e t e co po e t s a aged by se ect t a c d
elements dependent on that state.

Do you get a sense of what compound components are now?

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>

A poor way that the select element could have been designed

This is definitely not the best way to express this API as it makes passing
attributes to the child components almost impossible.

Quick quiz! #

1
What is a compound component?

COMPLETED 0%
1 of 2

With that in mind, let’s take a look at an example that’ll help you understand
and build your own compound components.
Example: Building an Expandable Component

In this lesson, we will set up an expandable component that is based on the compound component pattern

WE'LL COVER THE FOLLOWING

• What is an Expandable Component?


• Designing the API
• Building the Expandable Component
• Communicating the State to Child Components
• Quick Quiz!

What is an Expandable Component? #


We’ll be building an Expandable component. It will have a clickable header
that toggles the display of an associated body of content.

In the unexpanded state, the component would look like this


And this, when expanded

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.

In this case, here’s what we’re going for:

<Expandable>
<Expandable.Header> Header </Expandable.Header>
<Expandable.Icon/>
<Expandable.Body> This is the content </Expandable.Body>
</Expandable>

exposed API of expandable component

Unexpanded

Expanded
In the code block above, you’ll notice I have used expressions like this:
Expandable.Header

You can do this as well:

<Expandable>
<Header> Header </Header>
<Icon/>
<Body> This is the content </Body>
</Expandable>

It doesn’t matter which way as either way works. I have chosen


Expandable.Header over Header as a matter of personal preference. I find that
it communicates dependency on the parent component well, but that’s just my
preference. A lot of people don’t share the same preference and that’s
perfectly fine. Feel free to use whichever component looks good to you!

It’s your component, use whatever API looks good to you 🙂

Building the Expandable Component #


The Expandable component as the parent component will keep track of the
state, and it will do this via a boolean variable called expanded .

// state
{
expanded: true || false
}

The Expandable component needs to communicate the state to every child


component regardless of their position in the nested component tree.

Remember that the children are dependent on the parent compound


component for the state.

What is the best way to go about this?

Show Hint

Communicating the State to Child Components #


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.

provides state and callback to


update the state

consumes provided values


from parent

The state relationship of the expandable component

If that makes sense to you, here’s the starting point for the Expandable
component. We’ll be creating this in a file called Expandable.js

import React, { createContext } from 'react'

const ExpandableContext = createContext()


const { Provider } = ExpandableContext

const Expandable = ({children}) => {


return <Provider>{children}</Provider>
}

export default Expandable

There’s nothing spectacular going on in the code block above. There is no


output yet, so we haven’t put the code in one of our runnable widgets! Don’t
worry though, we’re getting to that part soon.

A context object is created, and the Provider component is 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.

Let’s create the expanded state value using useState .

import React, { createContext, useState } from 'react'

const ExpandableContext = createContext()


const { Provider } = ExpandableContext

const Expandable = ({children}) => {


// look here 👇
const [expanded, setExpanded] = useState(false)
return <Provider>{children}</Provider>
}

export default Expandable

With the expanded state variable created, let’s create the toggle updater
function to toggle the value of expanded — whether true or false .

import React, { createContext, useState } from 'react'

const ExpandableContext = createContext()


const { Provider } = ExpandableContext

const Expandable = ({children}) => {


const [expanded, setExpanded] = useState(false)
// look here 👇
const toggle = setExpanded(prevExpanded => !prevExpanded)
return <Provider>{children}</Provider>
}

export default Expandable

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 as what I’ve done above. The function passed to setExpanded
receives the previous value of expanded , i.e., prevExpanded and returns the
opposite of that, !prevExpanded

Quick Quiz! #
Before we move on, let’s take a quick quiz on what we’ve covered so far!

1 The first snippet is inherently worse than the second.

// First
<Expandable>
<Header> Header </Header>
<Icon/>
<Body> This is the content </Body>
</Expandable>

// Second
<Expandable>
<Expandable.Header> Header </Expandable.Header>
<Expandable.Icon/>
<Expandable.Body> This is the content </Expandable.Body>
</Expandable>

COMPLETED 0%
1 of 2

Let’s make some optimizations now. Catch you in the next lesson!
Preventing Unnecessary Renders

Let's optimize our code!

WE'LL COVER THE FOLLOWING

• Memoizing The Callback


• Quick Quiz!

Memoizing The Callback #


toggle acts as a callback function and it’ll eventually be invoked by
Expandable.Header . Let’s prevent any future performance issues by
memoizing the callback.

import React, { createContext, useState, useCallback } from 'react'

const ExpandableContext = createContext()


const { Provider } = ExpandableContext

const Expandable = ({children}) => {


const [expanded, setExpanded] = useState(false)
// look here 👇
const toggle = useCallback(
() => setExpanded(prevExpanded => !prevExpanded),
[]
)
return <Provider>{children}</Provider>
}

export default Expandable

Not sure how useCallback works? Have a look at this cheatsheet.

Once we have both expanded and toggle created, we can expose these via the
Provider ’s value prop.

import React, { createContext, useState, useCallback } from 'react'

const ExpandableContext = createContext()


const { Provider } = ExpandableContext

const Expandable = ({children}) => {

const [expanded, setExpanded] = useState(false)


// look here 👇
const toggle = useCallback(
() => setExpanded(prevExpanded => !prevExpanded),
[]
)
// look here 👇
const value = { expanded, toggle }
// and here 👇
return <Provider value={value}>{children}</Provider>
}

export default Expandable

This works, but the value reference will be different on every re-render
causing the Provider to re-render its children.

Let’s memoize the value .

import React, { createContext, useState, useCallback } from 'react'

const ExpandableContext = createContext()


const { Provider } = ExpandableContext

const Expandable = ({children}) => {


const [expanded, setExpanded] = useState(false)
// look here 👇
const value = useMemo(
() => ({ expanded, toggle }),
[expanded, toggle]
)
return <Provider value={value}>{children}</Provider>
}

export default Expandable

useMemo takes a callback that returns the object value {expanded, toggle} and
we pass an array dependency [expanded, toggle] . This means that the
memoized value remains the same unless the dependencies change.

We’ve done a great job so far!

Quick Quiz! #
Before we move on, let’s take a quick quiz on what we’ve covered so far!
1
What does memoization do in React?

COMPLETED 0%
1 of 2

In the next lesson, we’ll discuss an approach for avoiding unnecessary state
callbacks.
Handling State Change Callbacks

In this lesson, we'll learn how to use React hooks to handle state change callbacks.

WE'LL COVER THE FOLLOWING

• Triggering Callbacks Upon State Change


• Using useEffect
• Preventing Invocation Upon Mount
• Quick Quiz!

Triggering Callbacks Upon State Change #


Let’s borrow a concept from your 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.

Usually, the callback, e.g., this.props.onStateChange on line 4, is always


invoked with the current value of the updated state as shown below:

this.props.onStateChange(this.state.name)

Why is this important? This is good practice for 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:

const doSomethingPersonal = ({expanded}) => {


// do something really important after being expanded
}
<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 .

We will add this functionality to the Expanded component.

With class components, this is pretty straightforward. With functional


components, we need to do a little more work — not too much though :)

Using useEffect #
In most cases, when you want to perform a side effect within a functional
component, you should reach for useEffect .

The most natural solution might look like this:

useEffect(() => {
props.onExpanded(expanded)
}, [expanded])

The problem with this, however, is that the useEffect 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(() => {
// this function will always be invoked on mount
})

Preventing Invocation Upon Mount #


The functionality we seek requires that the callback passed by the user isn’t
invoked on mount.
How can we enforce this?

First, consider the naive solution below:

//faulty solution
...
let componentJustMounted = true
useEffect(
() => {
if(!componentJustMounted) {
props.onExpand(expanded)
componentJustMounted = false
}
},
[expanded]
)
...

What’s wrong with the code above?

Loosely speaking, the thinking behind the code is correct. You keep track of a
certain variable componentJustMounted , 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 false.

...
if (!componentJustMounted) {
onExpand(expanded)
}
...

Quick Quiz! #
Quiz yourself on what we’ve learned so far.
1 How do you update a component after a state change in traditional React
class components?

COMPLETED 0%
1 of 2

We have to somehow keep the value of componentJustMounted constant.

The solution to this problem lies in the next lesson. Catch you there!
Keeping Value Constant

WE'LL COVER THE FOLLOWING

• Using useRef to Ensure Value Remains Constant


• Final Implementation of Expandable
• Quick Quiz!

Using useRef to Ensure Value Remains Constant


#
The solution to this problem is simple. We can use the useRef hook to ensure
that a value stays the same throughout component’s entire lifetime.

Here’s how it works:

//correct implementation
const componentJustMounted = useRef(true)
useEffect(
() => {
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

The signature for useRef looks like this: useRef(initialValue) .

This means that a ref object is stored initially in


componentJustMounted.current with the current property set to true .
const componentJustMounted = useRef(true)

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 .

Final Implementation of Expandable #


Here’s what the final implementation of the Expandable component looks like:

import React, { createContext, useState, useCallback, useRef, useEffect, useMemo } from 'reac

const ExpandableContext = createContext()


const { Provider } = ExpandableContext

const Expandable = ({ children, onExpand }) => {


const [expanded, setExpanded] = useState(false)
const toggle = useCallback(
() => setExpanded(prevExpanded => !prevExpanded),
[]
)
const componentJustMounted = useRef(true)
useEffect(
() => {
if (!componentJustMounted.current) {
onExpand(expanded)
}
componentJustMounted.current = false
},
[expanded]
)
const value = useMemo(
() => ({ expanded, toggle }),
[expanded, toggle]
)
return (
<Provider value={value}>
{children}
</Provider>
)
}

export default Expandable


Quick Quiz! #
Quiz yourself on what we’ve learned so far.

Q
What is one problem with use useEffect ?

COMPLETED 0%
1 of 1

If you’ve followed along so far, that’s great! We’ve broken down the most
complex component in the bunch. Now, let’s move on to the child components.
Building the Compound Child Components

Now, let’s work on the Expandable component's child components and actually see some output!

WE'LL COVER THE FOLLOWING

• Child Components of Expandable


• The Header Child
• The Body Child
• The Icon Child
• Using the Expandable Component
• Current Look
• Quick Quiz!

Child Components of Expandable #


There are three child components for the Expandable component.

These child components need to consume values from the context object
created in Expandable.js .

To make this possible, we’ll do a little refactoring as shown below:


import React, { createContext, useState, useCallback, useRef, useEffect, useMemo } from 'reac

export const ExpandableContext = createContext()


const { Provider } = ExpandableContext

const Expandable = ({ children, onExpand }) => {


const [expanded, setExpanded] = useState(false)
const toggle = useCallback(
() => setExpanded(prevExpanded => !prevExpanded),
[]
)
const componentJustMounted = useRef(true)
useEffect(
() => {
if (!componentJustMounted.current) {
onExpand(expanded)
}
componentJustMounted.current = false
},
[expanded]
)
const value = useMemo(
() => ({ expanded, toggle }),
[expanded, toggle]
)
return (
<Provider value={value}>
{children}
</Provider>
)
}

export default Expandable

We export the context object, ExpandableContext , from Expandable.js .

Now, we may use the useContext hook to consume the values from the
Provider .

The Header Child #


Below is the Header child component fully implemented.

//Header.js
Header.js
import React, { useContext } from 'react'
import { ExpandableContext } from './Expandable'
Expandable.js
const Header = ({children}) => {
const { toggle } = useContext(ExpandableContext)
return <div onClick={toggle}>{children}</div>
}
export default Header
Simple, right?

It renders a div whose onClick callback is the toggle function for toggling
the expanded state within the Expandable parent component.

The Body Child #


Here’s the implementation for the Body child component:

// Body.js
Body.js
import { useContext } from 'react'
import { ExpandableContext } from './Expandable'
Header.js
const Body = ({ children }) => {
Expandable.js const { expanded } = useContext(ExpandableContext)
return expanded ? children : null
}
export default Body

Pretty simple as well.

The expanded value is retrieved from the context object and used within the
rendered markup. Line 7 reads like this: expanded, render children ,
otherwise, render nothing.

The Icon Child #


The Icon component is just as simple.

// Icon.js
Icon.js
import { useContext } from 'react'
import { ExpandableContext } from './Expandable'
Body.js
const Icon = () => {
Header.js const { expanded } = useContext(ExpandableContext)
return expanded ? '-' : '+'
Expandable.js }
export default Icon

It renders either + or - depending on the value of expanded retrieved from


the context object.

With all child components built, we can set them as Expandable properties.
See below:
import Header from './Header'
import Icon from './Icon'
import Body from './Body'
...

const Expandable = ({ children, onExpand }) => {


...
}
// Remember this is just a personal reference. It's not mandatory
Expandable.Header = Header
Expandable.Body = Body
Expandable.Icon = Icon

Using the Expandable Component #


Now, we can go ahead and use the Expandable component as designed:

<Expandable>
<Expandable.Header>React hooks</Expandable.Header>
<Expandable.Icon />
<Expandable.Body>Hooks are awesome</Expandable.Body>
</Expandable>

Current Look #
Here is the Expandable component so far!

// Body.js
import { useContext } from 'react'
import { ExpandableContext } from './Expandable'

const Body = ({ children }) => {


const { expanded } = useContext(ExpandableContext)
return expanded ? children : null
}
export default Body

Quick Quiz! #

1
Select all that apply for this question.

Why do we export the ExpandableContext from Expandable.js ?


COMPLETED 0%
1 of 5

This works but it has to be the ugliest component I’ve ever seen. We can do
better. Let’s try in the next lesson!
Manageable Styling for Reusable Components

Let's style our expandable component in this lesson!

WE'LL COVER THE FOLLOWING

• Styling the Header Component


• Adding CSS
• Allowing Changes in Default CSS
• Passing a Default className
• Removing False Values
• Quick Quiz!

Love it or hate it, styling (or CSS) is integral to how the web works.

While there’s a number of ways to style a React component - and I’m sure you
have a favorite - when you build reusable components, it’s always a good idea
to expose a frictionless API for overriding default styles.

Usually, I recommend having your components be styled via both style and
className props.

For example:

// this should work.


<MyComponent style={{name: "value"}} />
// and this.
<MyComponent className="my-class-name-with-dope-styles" />

Now, our goal isn’t just styling the component, but to make it as reusable as
possible. This means letting the consumer style the component whichever
way they want, whether that be using inline styles via the style prop, or by
passing some className prop.
Styling the Header Component #

Let’s begin with the Header child component; have a look at Header.js .

// Body.js
import { useContext } from 'react'
import { ExpandableContext } from './Expandable'

const Body = ({ children }) => {


const { expanded } = useContext(ExpandableContext)
return expanded ? children : null
}
export default Body

First, let’s change the rendered markup to a button . It’s a more accessible
alternative to the div used earlier.

// Body.js
import { useContext } from 'react'
import { ExpandableContext } from './Expandable'

const Body = ({ children }) => {


const { expanded } = useContext(ExpandableContext)
return expanded ? children : null
}
export default Body

Adding CSS #
We will now write some default styles for the Header component in a
Header.css file.

// Body.js
import { useContext } from 'react'
import { ExpandableContext } from './Expandable'

const Body = ({ children }) => {


const { expanded } = useContext(ExpandableContext)
return expanded ? children : null
}
export default Body

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 className used here, .Expandable-trigger .

To apply these styles, we need to import the CSS file and apply the
appropriate className prop to the rendered button .
// Body.js
import { useContext } from 'react'
import { ExpandableContext } from './Expandable'

const Body = ({ children }) => {


const { expanded } = useContext(ExpandableContext)
return expanded ? children : null
}
export default Body

Allowing Changes in Default CSS #


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
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 .

Here’s one way to do this:

// Body.js
import { useContext } from 'react'
import { ExpandableContext } from './Expandable'

const Body = ({ children }) => {


const { expanded } = useContext(ExpandableContext)
return expanded ? children : null
}
export default Body

Now, whatever className is passed to the Header component will be


combined with the default Expandable-trigger before being passed on to the
rendered button element.

Let’s consider how good the current solution is.

Passing a Default className #


First, if the className prop is null or undefined , the combinedClassName
variable will hold the value "Expandable-trigger null" or "Expandable-trigger
undefined".
To prevent this, be sure to pass a default className by using the ES6 default
parameters syntax as shown below in Header.js :

// Body.js
import { useContext } from 'react'
import { ExpandableContext } from './Expandable'

const Body = ({ children }) => {


const { expanded } = useContext(ExpandableContext)
return expanded ? children : null
}
export default Body

Having provided a default value, if the user still doesn’t enter a className , the
combinedClassName value will be equal to "Expandable-trigger " . Note the
empty string appended to the Expandable-trigger . This works because of the
way template literals work.

My preferred solution would be to do this:

// Body.js
import { useContext } from 'react'
import { ExpandableContext } from './Expandable'

const Body = ({ children }) => {


const { expanded } = useContext(ExpandableContext)
return expanded ? children : null
}
export default Body

The JavaScript join() method concatenates an array of strings into one single
array separated by a ‘separator’ passed as an argument to join() . So on line
8, we concatenate the string Expandable-trigger with the passed className
not separated by anything.

Removing False Values #


This solution handles the previously discussed edge cases. If you also want to
be explicit about removing null , undefined or any other false values, you can
do the following:

const combinedClassName = ['Expandable-trigger',className].filter(Boolean).join('')


I’ll stick with the simpler alternative, and provide a default for className via
default parameters.

Having said that, here’s the final implementation for Header .

// Body.js
import { useContext } from 'react'
import { ExpandableContext } from './Expandable'

const Body = ({ children }) => {


const { expanded } = useContext(ExpandableContext)
return expanded ? children : null
}
export default Body

So far, so good.

In case you were wondering, combinedClassName returns a string. Since strings


are compared by value, there’s no need to memoize this value with useMemo .

Quick Quiz! #

1
Why do we want the component to be stylable via both style and
className props?

COMPLETED 0%
1 of 7
So far, we’ve covered the className prop. What about the option to override
default styles by passing a style prop? Let’s write code to allow that in the

next lesson!
Custom Styling Options

We'll now learn how to override default styles!

WE'LL COVER THE FOLLOWING

• Styling
• Styling Body and Icon
• Body Component Before
• Body Component After
• The Icon Component Before
• The Icon Component After
• Styling Expandable
• Quick Quiz!

Styling #
So far, we’ve handled styling with the className prop. What about the option
to override default styles by passing a style prop? As of right now, we can’t
do that

Let’s fix that!

Instead of explicitly destructuring the style prop, we can pass any other prop
passed by the user to the button component.

// Body.js
import { useContext } from 'react'
import { ExpandableContext } from './Expandable'

const Body = ({ children }) => {


const { expanded } = useContext(ExpandableContext)
return expanded ? children : null
}
export default Body
Note the use of the rest parameter and spread syntax on lines number 5 and
10, respectively, in Header.js .

With this done, the Header component receives our default styles, while
allowing for change via the className or style props. Here’s how you would
pass styles to the Header component.

// override style via className


<Expandable.Header className="my-class">
React hooks
</Expandable.Header>

// override style via style prop


<Expandable.Header style={{color: "red"}}>
React hooks
</Expandable.Header>

Let’s try passing some styles! Let’s override via className first. Have a look at
App.js .

// Body.js
import { useContext } from 'react'
import { ExpandableContext } from './Expandable'

const Body = ({ children }) => {


const { expanded } = useContext(ExpandableContext)
return expanded ? children : null
}
export default Body

Of course, this does not have any effect right now because we have not
defined anything for this new className . Let’s override style via style prop
now.

// Body.js
import { useContext } from 'react'
import { ExpandableContext } from './Expandable'

const Body = ({ children }) => {


const { expanded } = useContext(ExpandableContext)
return expanded ? children : null
}
export default Body
Styling Body and Icon #

Now, I’ll go ahead and do the same for the other child components, Body and
Icon .

Body Component Before #

// before
const Body = ({ children }) => {
const { expanded } = useContext(ExpandableContext)
return expanded ? children : null
}

Body Component After #

Have a look at the Body.js file for changes to the body component. Note that
we’ve created a Body.css file.

import React, { useContext } from 'react'


import { ExpandableContext } from './Expandable'
import './Body.css'

const Body = ({ children, className = '', ...otherProps }) => {


const { expanded } = useContext(ExpandableContext)
const combinedClassNames = ['Expandable-panel', className].join('')

return expanded ? (
<div className={combinedClassNames} {...otherProps}>
{children}
</div>
) : null
}

export default Body

The Icon Component Before #

// before
const Icon = () => {
const { expanded } = useContext(ExpandableContext)
return expanded ? '-' : '+'
}

The Icon Component After #


Here, we are doing the same for the Icon component. Note that we’ve created
an Icon.css file as well.
import React, { useContext } from 'react'
import { ExpandableContext } from './Expandable'
import './Body.css'

const Body = ({ children, className = '', ...otherProps }) => {


const { expanded } = useContext(ExpandableContext)
const combinedClassNames = ['Expandable-panel', className].join('')

return expanded ? (
<div className={combinedClassNames} {...otherProps}>
{children}
</div>
) : null
}

export default Body

Notice the + and - move to the far right because of this styling!

Styling Expandable #
And finally, some styles for the parent component, Expandable .

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

Now we’ve got a beautiful reusable component!

1 of 2
2 of 2

Quick Quiz! #

1
What method of overriding is being used here?

<Expandable.Header style={{color: "red"}}>

COMPLETED 0%
1 of 3

This is great! Our component is stylable and is great for our use case. Let’s
look at how someone else might consume it for their usecase.
Custom Use of the Component

Not only have we made it beautiful, we’ve made it customizable as well.

WE'LL COVER THE FOLLOWING

• How Customizable is the Expandable Component?


• Final Look

How Customizable is the Expandable


Component? #
Have a look at what can be done with it!

1 of 2
2 of 2

And this doesn’t take a lot of code. Have a look.

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

Final Look #
You can go one step further to test if overriding styles via the style prop
works as well.

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

Yay! It works as expected.

We’ve built a component that other developers can style and use however
they want. But what if they want to extend it to add more functionality? We’ll
look at a design pattern in the next chapter that allows us to do exactly that!
What are Control Props?

Here's a quick overview of control props!

WE'LL COVER THE FOLLOWING

• Using Multiple Expandable Components


• Expanding Multiple Headers at Once
• Quick Quiz!

Using Multiple Expandable Components #


The compound component we built in the last section works great. However,
it’s quite limited in its extensibility.

Let me show you what I mean.

Assume another developer, who we’ll refer to as “user”, decided to use the
component we’ve built, but in a much different way.

This user has found some very good information they think is worth sharing.
Within their app, they store this information in an array:

// user's app
const information = [
{
header: 'Why everyone should live forever',
note: 'This is highly sensitive information ... !!!!'
},
{
header: 'The internet disappears',
note:
'I just uncovered the biggest threat...'
},
{
header: 'The truth about Elon musk and Mars!',
note: 'Nobody tells you this...'
}
]
In their App , they loop over the list of information and render our Expandable
component with its children as seen below:

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

Nothing out of the ordinary here.

When the user clicks any of the headers, the contents expand as designed. Try
clicking any one of the headers. One of the elements expands.

Expanding Multiple Headers at Once #


But what happens when another header is clicked?

Well, that header expands as well. This is exactly how the internals of our
Expandable component works.

As you can see from the output of the code above, both clicked headers
expand their content. Unfortunately, this is not the behavior the user seeks.

What the user really wants is behavior such that only one container expands
at a time. If the user clicks another header, it should open but the rest should
close.

Quick Quiz! #

1
What is the issue with the expandable component at the moment?
COMPLETED 0%
1 of 3

So, how do we 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 reality, you can
implement this functionality in any component you wish. In the next lesson,
we’ll learn how the control props pattern works.
How Control Props Work

In this lesson, we'll make the Expandable component work so that the elements expand one at a time

WE'LL COVER THE FOLLOWING

• Controlled vs. Uncontrolled Elements


• Implementing in Expandable
• How it Works
• Actual Implementation
• The Final Result
• Quick Quiz!

Controlled vs. Uncontrolled Elements #


Consider a simple input field.

<input />

In React, a controlled input element will be defined like this:

<input value={someValue} onChange={someHandler} />

Hmm, that’s different.

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 internally-managed state to be manageable from the outside via
props!

Implementing in Expandable #
How it Works #
The implementation is quite simple, but we need to understand how it works
before we delve into writing some code.

The default usage of the Expandable component is as shown below:

<Expandable>
<Expandable.Header>
{someHeader}
</Expandable.Header>
<Expandable.Icon />
<Expandable.Body>{someContent}</Expandable.Body>
</Expandable>

Within the Expandable component, the expanded value is internally managed


and communicated to the children.

If we want to make Expandable a controlled component, then we need to


accept a prop which defines the expanded value externally.

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.

Actual Implementation #
Now, let’s move on to the actual implementation.

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 = ({
shouldExpand, // 👈 take in the control prop
...otherProps
}) => {
// see check 👇

const isExpandControlled = shouldExpand !== undefined


...

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.

Now, the isExpandControlled variable is going to define how we handle a


couple of other things internally.

It all begins with the value that the Expandable component communicates to
the child elements.

// before
const value = useMemo(() => ({ expanded, toggle}), [
expanded, toggle
])

Before, we passed along the expanded and toggle values from the internals of
the Expandable component.

But we can’t do this now.

Remember, if the component is controlled, we want expanded to return the


shouldExpand prop passed in by the user.

We can extract this behavior into a getState variable defined below;

// Expanded.js
...
const getState = isExpandControlled ? shouldExpand : expanded
...

where shouldExpand is the user’s control prop and expanded is our internal
state.

Be sure to 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.
Before now, we had an internal toggle function.

// before. See toggle below 👇


const toggle = useCallback(
() => setExpanded(prevExpanded => !prevExpanded),
[]
)

Now, we can introduce a separate variable getToggle defined below:

// Expandable.js
...
const getToggle = isExpandControlled ? onExpand : toggle
...

If the component is controlled, we return the onExpand prop — a user defined


callback. If not, we return our internal implementation, toggle .

With these done, here’s the value communicated to the child elements:

...
const value = useMemo(() => ({ expanded: getState, toggle: getToggle }), [
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.

There’s one more cleanup to be performed.

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 component is controlled. Why? The user handles the toggle function
themselves when the component is controlled. They can choose to do anything
in the callback with the already controlled state value too.

Based on the explanation above, we need to make sure the useEffect call
doesn’t run when the component is controlled.
// Expandable.js
...
useEffect(
() => {
// run only when component is controlled.
// see !isExpandControlled 👇
if (!componentJustMounted && !isExpandControlled) {
onExpand(expanded)
componentJustMounted.current = false
}
},
[expanded, onExpand, isExpandControlled]
)
...

And that is it!

We’ve implemented the control props pattern.

The Final Result #


Here’s the final result.

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

I have yet to show you how it solves the user’s problem and how to use it in
the user’s app. We’ll learn that next! But before that, let’s take a quick quiz.

Quick Quiz! #

1
What makes a component a controlled component?
COMPLETED 0%
1 of 3

Hope you got those right! Catch you in the next lesson.
Using the Controlled Component

In this lesson, we'll learn how a user would use a controlled component from an app!

WE'LL COVER THE FOLLOWING

• Sending a Prop
• How The data-index Property Helps
• Setting the Active Index
• The Final Output
• Quick Quiz

Sending a Prop #
We reach out to the user and let them know that we have implemented a
pattern to cater for their specific use case.

Out of excitement, they get on to implement the change.

Here’s how.

Within the user app, they send a data-index prop to every Header element:

// before
<Expandable.Header />
// now
<Expandable.Header data-index={index} />

The index is retrieved from the iteration index of information .

function App () {
...
// see index below 👇
{information.map(({ header, note }, index) => (
...
)}
}
How The data-index Property Helps #
The returned element from the header gets a data-index property.

How does this help their cause?

Well, here’s the full usage of the controlled component.

// user's app
function App () {
const [activeIndex, setActiveIndex] = useState(null)
const onExpand = evt => setActiveIndex(evt.target.dataset.index)

return (
<div className='App'>
{information.map(({ header, note }, index) => (
<Expandable
shouldExpand={index === +activeIndex}
onExpand={onExpand}
key={index}
>
<Expandable.Header
style={{ color: 'red', border: '1px solid teal' }}
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 and should be active
at that given time.

This explains why they’ve defined the control prop shouldExpand as shown
below:

shouldExpand={index === +activeIndex} // 👈 the "+" converts the activeIndex to a number

Setting the Active Index #


Also, note the user’s onExpand callback.

const onExpand = evt => setActiveIndex(evt.target.dataset.index)

This is what’s invoked on every click, NOT our default toggle function. When
the user clicks on a header, the activeIndex state variable is set to the data-
index from the click target.

With this, the user implements their feature to their satisfaction. No two
headers are expanded at the same time.

The Final Output #


.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

Great job so far!

Quick Quiz #
Let’s take a quick quiz before we move on.

1
What does the === operator do in JavaScript?
COMPLETED 0%
1 of 3

With our controlled props API, we’ve made it relatively easy for the user to
control the state of the Expanded component. How convenient! Let’s build a
custom hook next.
Building a Custom Hook

In this lesson, you'll learn how to create a custom hook!

WE'LL COVER THE FOLLOWING

• Introduction
• useExpanded Custom Hook
• useEffectAfterMount Custom Hook
• Using Custom Hooks
• Quick Quiz!

Introduction #
Before we go into the other advanced patterns, it’s important to understand
the default way to share functionality with hooks — building a custom hook.

Only by building on this foundation can we take advantage of the other


advanced patterns we will discuss.

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. We’ll leave it as
Expandable — same as before.

Now, instead of having the logic for managing the expanded state in an
Expandable component that returns a Provider , we can just export 2 custom
hooks.

// Expandable.js
import useExpanded from '../useExpanded'
import useEffectAfterMount from '../useEffectAfterMount'
export { useExpanded as default, useEffectAfterMount }

useExpanded Custom Hook #


The useExpanded custom hook will now handle the logic for the expanded state
variable, and useEffectAfterMount will handle the logic for invoking a
callback only after mount.

So, shall we write these custom hooks?

Note that these custom hooks will pretty much use the same logic as the
Expandable compound component we had written earlier. The difference here
will be wrapping these in a custom hook.

Here’s the useExpanded custom hook:

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

If you would like a refresher on the internal logic of this custom hook, feel
free to look at the section on compound components.

What’s important is that we’ve wrapped this functionality in a useExpanded


function (aka custom hook) which returns the value we previously had in a
context Provider .

useEffectAfterMount Custom Hook #


We’ll do something similar with the useEffectAfterMount custom hook as
shown below:

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

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 on which the useEffect function relies.

Also, note that line 8 reads, return cb() . This is to handle unsubscriptions by
returning whatever is returned by the callback.

Great!

Using Custom Hooks #


Now that you’ve built these custom hooks, how would a typical consumer of
your OS library use these hooks?

Here’s one simple example. Have a look at App.js .

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

The consumer imports your useExpanded custom hook and invokes the
function to retrieve expanded and toggle .

With these values, they can render whatever they want. We provide the logic
and let the user render whatever UI they deem fit. By default, they render a
button.

This button invokes the exposed toggle function from the custom hook.
Based on the toggled expanded state, they render smileys.

Well, this is the user’s version of an expandable container.

Do you realize what we’ve done here?

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 so.

To use the useEffectAfterMount hook, the user needs to do something like the
following; have a look at App.js .

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

Now, whenever the button is clicked, they’ll get the logs in the console!

Quick Quiz! #
Let’s take a quiz. You know the drill.

1
What is the function cb() in the useEffectAfterMount custom hook?

COMPLETED 0%
1 of 4

Now that we know how to build and use custom hooks, let’s make the user’s
life even easier and provide them with some default UI elements.
Providing Default UI Elements

We'll now make our open source library more user-friendly and convenient by allowing export of default UI
elements,

WE'LL COVER THE FOLLOWING

• Exporting Default Elements


• The Final Result
• Quick Quiz!

Exporting Default Elements #


If the user is reaching out for an Expandable OS library, like the one we made
in the last lesson, they’ll need the custom logic you’ve extracted into a
reusable hook. They’ll also need to render some UI elements.

You could also export some default Header , Body and Icon elements built to
make their styling more configurable.

This sounds as if 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)
const Body = ({ children, className = '', ...otherProps }) => {
...
// look here 👇
const { expanded } = useContext(ExpandableContext)
return ...
}
// now (takes in expanded as props)
const Body = ({
children, className = '', expanded, ...otherProps }) => {
...
return ...
}
The same can 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 with UIs as much.

The Final Result #


.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

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.

Quick Quiz! #
Test yourself on this lesson!

1
Why are we using our custom UI elements here?

COMPLETED 0%
1 of 2
We’ve done a brilliant job building this imaginary OS library, huh? In the
following chapters, we’ll incorporate even more advanced patterns that users
will find incredibly useful.
What is the Props Collection Pattern?

In this lesson, we'll have a quick overview of the props collection pattern!

WE'LL COVER THE FOLLOWING

• Commonalities Across Component Usage


• Quick Quiz!

Commonalities Across Component Usage #


Regardless of the components, you build and who uses them, some things will
remain the same for all users of the component.

How can we know this?

Well, consider the Expandable component we’ve worked so hard on.

Regardless of the UI solution the user decides to go for, they’ll always need an
onClick handler for the “toggle element”.

const { toggle, expanded } = useExpanded()


// user 1.
<button onClick={toggle}>Click to view awesomeness...</button>
// user 2.
<div onClick={toggle}> Burn the place down 🔥</div>
// user 3.
<img onClick={toggle} alt="first image in carousel"/>

You get the point.

Regardless of the element they choose to render, every user still needs to
handle the onClick callback.

If these users care about accessibility at all, they will do this:

const { toggle, expanded } = useExpanded()


// user 1.
<button onClick={toggle} aria-expanded={expanded}>Click to view awesomeness...</button>
// user 2.

<div onClick={toggle} aria-expanded={expanded}> Burn the place down 🔥</div>


// user 3.
<img onClick={toggle} aria-expanded={expanded} alt="first image in carousel"/>

Now, that’s two props.

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?

These are the questions that the props collection pattern answers with a
resounding yes!

A props collection is a collection of “common props” associated with a


custom hook or component.

Quick Quiz! #

1
What’s the main advantage of the props collection pattern?

COMPLETED 0%
1 of 2
In the next lesson, we’ll look at this pattern in action!
The Props Collection Pattern in Practice

Let's apply the props collection pattern to our expandable component!

WE'LL COVER THE FOLLOWING

• The Props Collection


• Final Output

The Props Collection #


Take a look at the props collection for the useExpanded hook:

export default function useExpanded () {


const [expanded, setExpanded] = useState(false)
const toggle = useCallback(
() => setExpanded(prevExpanded => !prevExpanded),
[]
)
// look here 👇
const togglerProps = useMemo(
() => ({
onClick: toggle,
'aria-expanded': expanded
}),
[toggle, expanded]
)

...
return value
}

Within useExpanded we’ve created a new memoized object, togglerProps ,


which includes the onClick and aria-expanded properties.

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.

export default function useExpanded () {


...
// look here - togglerProps has been included 👇
const value = useMemo(() => ({ expanded, toggle, togglerProps }), [
expanded,
toggle,
togglerProps
])

return value
}

Final Output #
With togglerProps now exposed, it can be consumed by any user as shown
below. Have a look at useExpanded.js .

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

This works just like before, except that the user didn’t have to manually write
the “common props” for the toggle element.
Here is the expected output of the app. aria attribute also added to the DOM element.

In the next chapter, we will consider an alternative (perhaps more powerful)


solution to the same problem the props collection pattern aims to solve.
What is the Prop Getters Pattern?

Here's a quick overview of the prop getters pattern!

WE'LL COVER THE FOLLOWING

• Props Collection vs. Prop Getters Pattern


• Refactoring the Previous Solution
• Putting It All Together
• Quick Quiz

Props Collection vs. Prop Getters Pattern #


In JavaScript, all functions are objects. However, functions are built to be
more customizable and reusable.

Consider the following:

const obj = {name: "React hooks"}

console.log(obj)

If you wanted to create the same object but allow for some level of
customization, you could do this:

const objCreator = n => ({name:n})

Now, this objCreator function can be called multiple times to create different
objects as shown below:

const objCreator = n => ({name:n})


const obj1 = objCreator("React hooks")
const obj2 = objCreator("React hooks mastery")

const obj3 = objCreator("React hooks advanced patterns")

console.log(obj1)
console.log(obj2)
console.log(obj3)

Why have I chosen to explain this?

Because this is the difference between the props collection pattern and prop
getters pattern.

While props collection relies on providing an object, prop getters expose


functions that can be invoked to create a collection of props.

Refactoring the Previous Solution #


Because functions can be customized, using a prop getter allows for more
interesting use cases.

Let’s start by refactoring the previous solution to use a props getter.

// before
const togglerProps = useMemo(
() => ({
onClick: toggle,
'aria-expanded': expanded
}),
[toggle, expanded]
)

// now
const getTogglerProps = useCallback(
() => ({
onClick: toggle,
'aria-expanded': expanded
}),
[toggle, expanded]
)

Instead of togglerProps , we now create a memoized function,


getTogglerProps , that returns the same props collection.

We’ll expose this via the returned value variable within the useExpanded hook
as shown below:

export default function useExpanded () {


...
const getTogglerProps = useCallback(
() => ({
onClick: toggle,
'aria-expanded': expanded
}),
[toggle, expanded]
)
// look here 👇
const value = useMemo(() => ({ expanded, toggle, getTogglerProps }), [
expanded,
toggle,
getTogglerProps
])
return value
}

Now, we need to update how the props collection is consumed.

// before
<button {...togglerProps}>Click to view awesomeness...</button>
// now
<button {...getTogglerProps()}>Click to view awesomeness...</button>

The user would do this in their app.

Putting It All Together #


.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

And that’s it! The user’s app works just like before.

Quick Quiz #
Let’s take a quick quiz before moving on!

1
What is the main difference between the prop getters pattern and the
props collection pattern?
COMPLETED 0%
1 of 2

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 and we’ll pass in some arguments soon.


Additional User Props

In this lesson, we'll learn how users can pass additional props to prop getters!

WE'LL COVER THE FOLLOWING

• Passing Additional Props to Prop Getters


• The Final Output
• Quick Quiz

Passing Additional Props to Prop Getters #


With the prop getter, getTogglerProps , we can cater to other props the user
may be interested in passing down to the toggle element.

For example, we currently have this usage by the user:

<button {...getTogglerProps()}>Click to view awesomeness...</button>

If the user needs some more props that we haven’t catered for in the prop
collection, they could do this:

<button id='my-btn-id' aria-label='custom toggler' {...getTogglerProps()}>


Click to view awesomeness...
</button>

Passing id and aria-label works just fine.


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',
'aria-label': 'custom toggler'
})}
>
Click to view awesomeness...
</button>

It’s arguably more expressive. So, let’s extend the getTogglerProps function to
handle this use case.
...
const getTogglerProps = useCallback(

({ ...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 to in the default props collection we expose.

The Final Output #


Here it is all put together.

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

How interesting!

Quick Quiz #
Let’s take a quick quiz before moving forward

Q
What would happen if we passed custom props like so?

({ customProps }) => ({
...

COMPLETED 0%
1 of 1

In the next section, we’ll consider an even more interesting case.


Handling Props with the Same Property Name

In this lesson, we'll see how two prop properties with the same name can be invoked at once.

WE'LL COVER THE FOLLOWING

• Overriding the User’s onClick Handler


• Invoking Both User’s and Internal onClick
• How callFunctionsInSequence Works
• Quick Quiz!

We allowed passing custom props into the getToggler props getter. Now, what
if the user wanted to pass in a custom onClick prop?

Here’s what I mean:

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

If the user does this, it completely overrides the onClick handler within the
useExpanded custom hook and breaks the functionality of the app as you can
see in the output.

The button no longer expands the element, but the user’s custom click handler
is invoked whenever you click the button.

Why is this the case?

This is because we have an onClick property in the returned object, but it is


overridden by the user.
// useExpandable.js
...
const getTogglerProps = useCallback(

({ ...customProps }) => ({
onClick: toggle, // this is overriden
'aria-expanded': expanded,
// customProps overrides the onClick above
...customProps
}),
[toggle, expanded]
)

Overriding the User’s onClick Handler #


If you move the position of the internal onClick handler, you can override the
user.

const getTogglerProps = useCallback(


({ onClick, ...props } = {}) => ({
'aria-expanded': expanded,
onClick: callFunctionsInSequence(toggle, onClick),
...props
}),
[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.

How can we achieve this?

Invoking Both User’s and Internal onClick #


Well, we could have the internal onClick invoke the user’s click handler as
well.

Here’s how:

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

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 !

...
onClick: callFunctionsInSequence(toggle, onClick)

I’ve called this function callFunctionsInSequence so that it’s easy to


understand what happens when called.

Also, note how we destructure the user’s onClick handler and use the rest
parameter to handle the other props passed in. To avoid getting an error if no
object is passed in by the user, we set a default parameter value of {} .

...
// note destructuring and default parameter value of {}
({ onClick, ...props } = {}) => ({

})

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 cannot break if any of the function arguments are undefined
either.

Here’s one solution to this problem:

const callFunctionsInSequence = (...fns) => (...args) =>


fns.forEach(fn => fn && fn(...args))

When we do this:

...
onClick: callFunctionsInSequence(toggle, onClick) // this invokes callFunctionsInSequence

The return value saved in the onClick object property is the returned
function from invoking callFunctionsInSequence :
(...args) => fns.forEach(fn && fn(...args))

If you remember from basic React, we always attach click handlers like this:

...
render() {
return <button onClick={this.handleClick} />
}

We attach the click handler handleClick , but in the handleClick declaration,


we expect an evt argument to be passed in at invocation time.

// see event object (evt)


handleClick = evt => {
console.log(evt.target.value)
}
...

How callFunctionsInSequence Works #


Now back to the returned function from callFunctionsInSequence :

(...args) => fns.forEach(fn && fn(...args))

The function receives whatever arguments are passed into the function,
(...args) , and invokes all function parameters with these arguments if the
function is not false. fn && fn(...args) .

In this case, the argument received by the function will be the event object
and it will be passed down to both toggle and onClick as arguments.
The callFuncstionsInSequence Function

The full implementation of the custom hook now looks like this:

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

Quick Quiz! #

Q
What is the suggested solution to the issue of props with the same
property name?
COMPLETED 0%
1 of 1

With this addition, our internal click handler, toggle , and the user’s custom
handler, onClick , are both invoked when the toggle button is clicked!

How amazing! We wouldn’t be able to cater for this case with just a prop
collection object. Thanks to prop getters, we have more power to provide
reusable components.

From the next chapter on, we’re going to be looking at a new pattern: State
Initializers. Catch you in the next lesson!
What is the State Initialization Pattern?

Here's a quick introduction to patterns in React with state initialization!

WE'LL COVER THE FOLLOWING

• Setting the Value of the State


• Quick Quiz!

Setting the Value of the State #


To initialize means to set the value of something. Well, technically,
initialization is setting the value of something right when it is created/defined
and any subsequent value setting is referred to as ‘assignment’. However,
we’re going with the first definition to make things simpler. 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 the
“value of the state” every single time. It mostly allows for setting initial state
resetting it.

This is not the same as having full control over setting the state value, but it
offers similar benefits as you’ll see soon.

In our implementation of the custom hook, we’ve done the following:

export default function useExpanded () {


// look here 👇
const [expanded, setExpanded] = useState(false)
...
}

We’ve assumed that the initial state passed to the useState call will be false
every single time.
That may not be the case.

Let’s have this value controllable from an argument the user can pass in. Let’s
call this parameter initialExpanded as seen below:

export default function useExpanded (initialExpanded = false) {


const [expanded, setExpanded] = useState(initialExpanded)
...
return value
}

Now the user can pass in initialExpanded into the useExpanded custom hook
call to decide the initial state of the expanded component.

Here’s an example where a user may want the terms and conditions content
expanded by default:

// user's app
function App () {
// look here - "true" passed into hook call 👇
const { expanded, toggle } = useExpanded(true)
return (
...
)
}

Terms and conditions expanded by default

With our previous setup, this was not an easy feat for the user because we had
internally hardcoded the initial state as false .

As you can see, this is a simple but helpful pattern.

Quick Quiz! #
Time for a quiz!

Q
What key problem does this pattern address?

COMPLETED 0%
1 of 1

In the next lesson, we’ll have a look at how the state can be reset!
Resetting the State

WE'LL COVER THE FOLLOWING

• Why Reset the State?


• How to Reset State?
• The Output
• Quick Quiz!

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 time, i.e., a reset callback they can invoke to reset the state to the
initial default state they provided.

This is useful in many different use cases.

Why Reset the State? #


Let’s consider a really trivial example.

Assume the terms and conditions 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. A reader could click to revert the expandable content to the
initial closed state for which they may want to close the expandable content
and perform some cleanups.

We could provide the user a reset callback for this, right?

Even though this particular example isn’t the most realistic for the reset
functionality, in larger applications you can solve the problem using the same
method discussed here.

How to Reset State? #


So, here comes the solution.

All the reset function should do is set the “expanded” state back to the
default provided, i.e., initialExpanded

Here’s a simple implementation:

export default function useExpanded (initialExpanded = false) {


const [expanded, setExpanded] = useState(initialExpanded)
const reset = useCallback(
() => {
// look here 👇
setExpanded(initialExpanded)
},
[initialExpanded]
)
...
}

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.

Easy enough. However, there’s still something we need to do.

Remember, this consumer of our custom hook wants to close the terms and
conditions body and also perform some cleanup/side effect.

How will this user perform the cleanup after a reset? We need to cater to 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.

// we made this possible 👇


useEffectAfterMount(
() => {
// user can perform any side effect here 👇
console.log('Yay! button was clicked!!')
},
[expanded]
)

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.

useEffectAfterMount accepts a callback to be invoked and an array


dependency that determines when the effect function is called except when
the component just mounts.

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.

For a reset what do we do?

Ultimately, here’s what we want the user to do.

// user's app
const { resetDep } = useExpanded(false)
useEffectAfterMount(
() => {
console.log('reset cleanup in progress!!!!')
},
[resetDep]
)
...

We need to provide a reset dependency the user can pass into the
useEffectAfterMount array dependency.

That’s the end goal.

Can you think of a solution to this? What do we expose as a reset 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 has 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.

Here’s the implementation of that:

// useExpanded.js
...
const [resetDep, setResetDep] = useState(0)
const reset useCallback(
() => {
// perform actual reset
setExpanded(initialExpanded)
// increase reset count - call this resetDep
setResetDep(resetDep => resetDep + 1)
},
[initialExpanded]
)
...

We can then expose resetDep alongside other values.

// useExpanded.js
...
const value = useMemo(
() => ({
expanded,
toggle,
getTogglerProps,
reset,
resetDep
}),
[expanded, toggle, getTogglerProps, reset, resetDep]
)
return value
...

The Output #
This works just as expected!

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

This is a decent solution. It works fine and is easy to reason about.

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.

Quick Quiz! #
Time for a quiz.

Q
Why is a reset callback important?

COMPLETED 0%
1 of 1

We’ll have a look at this in the next lesson, however, note that this is just
another case of personal preference. There’s nothing technically wrong with
the solution above.
Getting Rid of Extra State Variables

Let's clean up the useExpanded.js le by removing extra state variables

WE'LL COVER THE FOLLOWING

• Using a ref Object


• The Final Look
• Quick Quiz!

Using a ref Object #


As discussed in the last lesson, I prefer to provide the resetDep via a ref
object instead of creating a state variable. 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 resetDep remains the same.

So, here’s the implementation with a ref object:

// useExpanded.js
...
const resetRef = useRef(0)
const reset = useCallback(
() => {
// perform actual reset
setExpanded(initialExpanded)
// update reset count
++resetRef.current
},
[initialExpanded]
)
...
// expose resetDep within "value"
...
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 initializer pattern you offer the
user the ability to decide the 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 state
and use this within your custom hook.

If you also choose to manage your internal state via useReducer , you can still
apply this pattern.

The point is, regardless of your implementation, the goal remains the same; to
provide the user with the ability to decide the initial state and allow them to
reset and invoke custom callbacks after a reset is made.

The Final Look #


Here is the “Terms and Conditions” expandable object put together so far.

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

Quick Quiz! #
Lets take a quick quiz before moving on!

Q
Why have we decided not to keep resetDep as a state variable?
COMPLETED 0%
1 of 1

In the next chapter, 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 about!
What Is the State Reducer Pattern?

Let's get started with the reducer pattern!

WE'LL COVER THE FOLLOWING

• Why the State Reducer Pattern?


• State Updating In Regular Patterns
• State Updating in the Reducer Pattern
• Quick Quiz!

Why the State Reducer Pattern? #


The state reducer pattern is the final and perhaps the most advanced
pattern that we’ll be discussing in this course. Don’t let that scare you. I’ll take
my time to explain how it really works while explaining why it matters too.

When we write custom hooks or components, they most likely have some way
to keep track of the internal state. With the state reducer pattern, we give
control to the user of our custom hook/component on how the state is updated
internally.

State Updating In Regular Patterns #


The technical term for this is called "inversion of control”. In layman terms, it
means a system that allows the user to control how things work internally
within your API.

Let’s look at how this works conceptually.

The typical flow for communicating updates to the user of your custom hook
or component looks like this:
Communicating updates in custom hooks/components

Within your custom hook, you call useState or useReducer or setState to


update state internally. Once the update is made, the new state value is
communicated to the user.

This is how all the patterns we’ve looked at so far have worked.

State Updating in the Reducer Pattern #


With the state reducer pattern, there’s a significant change in how internal
state updates are made.

Here’s an illustration:
New internal state updation pattern

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.

The internal update is controlled by the user. They have a say!

Quick Quiz! #
Let’s take a quiz on how the state reducer pattern works.
1
How do updates occur in other patterns?

COMPLETED 0%
1 of 2

Let’s dive right into the implementation-level details of this pattern in the next
lesson!
Refactoring to `useReducer`

Let’s modify our custom hook to use `useReducer` instead of `useState`.

WE'LL COVER THE FOLLOWING

• Refactoring to useReducer
• Reducers
• End Result of This Chapter

Refactoring to useReducer #
The most intuitive way to make the state reducer pattern work is using the
useReducer hook to manage the 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.

Here’s what’s to be changed.

First, we’ll change the state from just a boolean value, true or false , to an
object like this: {expanded: true || false} .

// useExpanded.js
export default function useExpanded (initialExpanded = false) {
const initialState = { expanded: initialExpanded }
...
}

Once we do that, we’ll invoke useReducer to manage the state.

// useExpanded.js
// before
...
const [expanded, setExpanded] = useState(initialExpanded)

// now
const [{ expanded }, setExpanded] = useReducer(internalReducer, initialState)
...

Note how we need to destructure expanded from the state object in the new
implementation on line 7. 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)

Reducers #
If you’ve worked with 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 exists in 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 a visual guide that will make the concepts 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
Here’s the implementation:

// useExpanded.js
const internalReducer = (state, action) => {
switch (action.type) {
case useExpanded.types.toggleExpand:
return {
...state,
expanded: !state.expanded //toggle expand state property
}
case useExpanded.types.reset:
return {
...state,
expanded: action.payload // reset expanded with a payload
}
default:
throw new Error(`Action type ${action.type} not handled`)
}
}

export default function useExpanded (initialExpanded = false) {


const [{ expanded }, setExpanded] = useReducer(internalReducer, initialState)
...
}

Remember, I said actions typically have a type property. The reducer checks
this type property and returns a new state based on the type.

To stay consistent and prevent unwanted typos, the available types for the
useExpanded custom hook are centralized within the same file.

// useExpanded.js
...
useExpanded.types = {
toggleExpand: 'EXPAND',
reset: 'RESET'
}

That explains why the types are used this way in the code as shown below:

const internalReducer = (state, action) => {


switch (action.type) {
case useExpanded.types.toggleExpand: //👈 look here
return {
...state,
expanded: !state.expanded
}
...
}

Note that the returned value from the reducer represents the new state of the
ote t at t e etu ed a ue o t e educe ep ese ts t e e state o t e
expanded state. For example, here’s the returned value for the 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,
payload: initialExpanded // pass a payload "initialExpanded"
})

setExpanded is now a “dispatcher” so it’s called with an action object. An


action object has a type and a payload. Redux users must be familiar with the
payload property which keeps a reference to a value required by this action to
make a state update.

Note how the reducer returns the new state based on this dispatched reset
action:

...
case useExpanded.types.reset:
return {
...state,
expanded: action.payload // reset expanded with the payload
}
...

Here, the payload passed in is the initialExpanded variable.

We’ve made decent progress!

Below is a quick summary of the changes made:

// useExpanded.js
export default function useExpanded (initialExpanded = false) {
// keep initial state in a const variable
// NB: state is now an object e.g {expanded: false}
const initialState = { expanded: initialExpanded }

// the useReducer call for state updates 👇


const [{ expanded }, setExpanded] = useReducer(internalReducer, initialState)
// 👆 Note how we destructured state variable, expanded

// setExpanded is now a dispatcher. Toggle fn refactored 👇


const toggle = useCallback(
// Every setExpanded call should be passed an object with a type
() => setExpanded({ type: useExpanded.types.toggleExpand }),
[]
)
...
}

The available types for state updates are then centralized to prevent string
typos.

// useExpanded.js
...
useExpanded.types = {
toggleExpand: 'EXPAND',
reset: 'RESET'
}

Here’s how the reset is performed internally:

// before
setExpanded(initialExpanded)
// now
...
setExpanded({
type: useExpanded.types.reset,
payload: initialExpanded // pass a payload "initialExpanded"
})
...

The result of this is a complete refactor of useReducer — no additional


functionality has been added.

Does anything confuse you?

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.

Right now, the custom hook is invoked like this:

// user's app
...
const { expanded, toggle, reset, resetDep} = useExpanded(false)

Where the value passed to useExpanded is the initial expanded state.

Remember 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 () {
const { expanded, toggle, reset, resetDep } = useExpanded(
false,
appReducer // 👈 the user's reducer
)
...
}

End Result of This Chapter #


Before we go ahead with the rest of the implementation, let’s see what the
user will be able to do at the end. Have a look!

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

Let’s learn how this particular user used our custom hook piece-by-piece in
the rest of this chapter!
The User’s Application

In this lesson, we'll look at how our custom hook can be used in an application

WE'LL COVER THE FOLLOWING

• Scenario
• Calling Toggle When Header is Clicked
• Viewing the Secret Document
• What We Have So Far

Scenario #
Our user is a hacker who has found proof of a big conspiracy on Mars. To
share this with the world, they want to use our useExpanded hook and the
default UI elements we provide to build the following expandable view:

That’s the easy part.

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. After

that, the reader can click all they want on the header but the content won’t be
expanded.

Do you see why the user needs the state reducer pattern?

Calling Toggle When Header is Clicked #


By default, whenever the Header element is clicked, the hacker calls the
toggle function.

...
// look here 👇
<Header toggle={toggle} style={{ border: '1px dotted red' }}>
They've been lying to you
</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 not to trigger a state update.

We need to cater to this use case.

Viewing the Secret Document #


In the meantime, 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>
Click to view the conspiracy <br />
<button onClick={reset}> View secret </button>
</p>
</Body>
</div>
</section>

The reset callback resets the expanded state to the initial expanded state of
false provided by the hacker.

...
const { expanded, toggle, reset, resetDep} = useExpanded(
false)

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 based on the resetDep which changes
when the user clicks the button.

...
useEffectAfterMount(
() => {
// open secret in new tab 👇
window.open('https://leanpub.com/reintroducing-react', '_blank')
// can do more e.g 👉 persist user details to database
},
[resetDep]
)
...

So far the hacker is happy with the API we’ve provided. We just need to
handle their last use case for preventing further state updates on clicking the
Header element after a reader has viewed the secret once.

What We Have So Far #


Have a look!

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

In the next lesson, we’ll apply the state reducer pattern to our application.
Implementing the State Reducer Pattern

Let’s integrate the state reducer pattern into the scenario we studied earlier

WE'LL COVER THE FOLLOWING

• Implementation
• Resolving User and Internal Implementation
• The App So Far
• Quick Quiz!

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.

The hacker will then check their own application state. If the reader has
already viewed the secret resource, the hacker will communicate to our
custom hook NOT to allow a state update.

There it is. The power of the state reducer pattern, giving control of the
internal state updates to the user.

Implementation #
Now, let’s write the technical implementation.

First, we’ll expect a second argument from the hacker, their own reducer.

// Note the second parameter to our hook, useExpanded 👇


function useExpanded (initialExpanded = false, userReducer) {
...
}
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.

// Note the use of our "internalReducer"


const [{ expanded }, setExpanded] = useReducer(internalReducer, initialState)
...

Resolving User and Internal Implementation #


The problem with this is that our internal reducer always returns our internal
proposed new state. This isn’t the behavior 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.

So, what do we do?

Instead of passing our internalReducer to the useReducer call, let’s pass in


another reducer we’ll call resolveChangesReducer .

The sole purpose of resolveChangesReducer is to resolve changes between our


internal implementation and what the user suggests.

Every reducer takes in state and action, right?

The implementation of resolveChangesReducer begins by receiving the state


and action, and holding a reference to our internal change.

const resolveChangesReducer = (currentInternalState, action) => {


// look here 👇
const internalChanges = internalReducer(currentInternalState, action)
...
}

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.
const resolveChangesReducer = (currentInternalState, action) => {
const internalChanges = internalReducer(currentInternalState, action)
// look here 👇
const userChanges = userReducer(currentInternalState, {
...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
}

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. The user can either return our changes if they’re happy with them
OR override whatever changes they need to.

With a reference to the user changes held, the resolveChangesReducer now


returns the proposed changes from the user NOT ours.

// useExpanded.js
...
const resolveChangesReducer = (currentInternalState, action) => {
const internalChanges = internalReducer(currentInternalState, action)
const userChanges = userReducer(currentInternalState, {
...action,
internalChanges
})

// look here 👇
return userChanges
}
...

The return value of a reducer depicts new state. resolveChangesReducer


returns the user’s changes as the new state!
etu s t e use s c a ges as t e e state!

That’s all there is to do!

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 at the end of the
lesson if you care about the entire source code (and you should!)

// necessary imports 👇
import { useCallback, useMemo, useRef, useReducer } from 'react'
...
// our internal reducer 👇
const internalReducer = (state, action) => {
switch (action.type) {
case useExpanded.types.toggleExpand:
return {
...state,
expanded: !state.expanded
}
case useExpanded.types.reset:
return {
...state,
expanded: action.payload
}
default:
throw new Error(`Action type ${action.type} not handled`)
}
}
// the custom hook 👇
export default function useExpanded (initialExpanded = false, userReducer) {
const initialState = { expanded: initialExpanded }
const resolveChangesReducer = (currentInternalState, action) => {
const internalChanges = internalReducer(
currentInternalState,
action
)
const userChanges = userReducer(currentInternalState, {
...action,
internalChanges
})
return userChanges
}
// the useReducer call 👇
const [{ expanded }, setExpanded] = useReducer(
resolveChangesReducer,
initialState
)
...
// 👇 value returned by the custom hook
const value = useMemo(
() => ({
expanded,
toggle,
getTogglerProps,
reset,
resetDep: resetRef.current
}),
[expanded, toggle, getTogglerProps, reset]
)
return value
}
// 👇 the available action types
useExpanded.types = {
toggleExpand: 'EXPAND',
reset: 'RESET'
}

Not so hard to understand, huh?

With the pattern now implemented internally, here’s how the hacker took
advantage to implement their feature.

// the user's app 👇


function App () {
// ref holds boolean value to decide if user has viewed secret or not
const hasViewedSecret = useRef(false) // 👈 initial value is false
// hacker calls our custom hook 👇
const { expanded, toggle, reset, resetDep = 0 } = useExpanded(
false,
appReducer // 👈 hacker passes in their reducer
)

// The user's reducer


// Remember that action is populated with our internalChanges
function appReducer (currentInternalState, action) {
// dont update "expanded" if reader has viewed secret
// i.e hasViewedSecret.current === true
if (hasViewedSecret.current) {
// object returned represents new state proposed by hacker
return {
...action.internalChanges,
// override internal update
expanded: false
}
}

// else, hacker is okay with our internal changes


return action.internalChanges
}

useEffectAfterMount(
() => {
// open secret in new tab 👇
window.open('https://leanpub.com/reintroducing-react', '_blank')
//after viewing secret, hacker sets the ref value to true.
hasViewedSecret.current = true
},
[resetDep]
)
...
}

That’s a well-commented code snippet, you agree?


I hope you find it clear enough to understand how a user may take advantage
of the state reducer pattern you implement in your custom hooks/component.

The App So Far #


With this implementation, the hacker’s request is fulfilled! Here’s the app in
action.

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

Quick Quiz! #
Let’s take a quiz.

1 What is the second argument in the useExpanded function?

function useExpanded (initialExpanded = false, userReducer) {


...
}

COMPLETED 0%
1 of 2

Now that we understand the meat of this pattern, there are a couple of things
that we need to work on to clean the code up. See you in the next lesson!
Updating Internal State

Let's see how a user could choose to dictate how internal state is updated.

WE'LL COVER THE FOLLOWING

• What We Have Now


• Quick Quiz!

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 making their own state
changes only when the action type refers to a toggle expand.

Here’s what I mean:

// user's app
...
function appReducer (currentInternalState, action) {
if (
hasViewedSecret.current &&
// look here 👇
action.type === useExpanded.types.toggleExpand
) {
return {
...action.internalChanges,
// 👇 override internal update
expanded: false
}
}
return action.internalChanges
}
...

By doing this we may extend our custom hook’s functionality to expose an


override callback like this:

// useExpanded.js
...
// exposed callback 👇
const override = useCallback(
() => setExpanded({ type: useExpanded.types.override }),

[]
)

...
useExpanded.types = {
toggleExpand: 'EXPAND',
reset: 'RESET',
override: 'OVERRIDE' // 👈 new "OVERRIDE" type
}

The override type is then handled via our internal reducer as follows:

...
case useExpanded.types.override:
return {
...state,
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.

// override being retrieved for our hooks call 👇


const { expanded, toggle, override, reset, resetDep = 0 } = useExpanded(
false,
appReducer
)

...
// button is only shown if reader has viewed secret
{ hasViewedSecret.current && (
<button onClick={override}>
Be redeemed to view secret again
</button>
)}
Note how the button’s callback function is override .

This allows for the expanded container to be open — and letting the user view
the secret one more time.

What We Have Now #


Here’s how it works so far.

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

Quick Quiz! #
Let’s take a quiz.

Q
What does the override functionality allow us to do?

COMPLETED 0%
1 of 1

We’re almost done with the course! Let’s have a look at some quick cleanups
in the next lesson and then we’re done!
Cleanups

You nally made it! We're almost done. Let's clean a few things up.

WE'LL COVER THE FOLLOWING

• Using the Custom Function Without a Reducer


• Providing a Default Reducer
• The Final Result
• Quick Quiz!

Using the Custom Function Without a Reducer #


Right now, if some user other than this crazy hacker uses our custom function
without passing a reducer, their app breaks.

useExpanded(false) // 👈 user doesn't need reducer feature

Try running this below to see how not passing a reducer breaks the app.

.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

Running the code above will get you an error like this:
Providing a Default Reducer #
Let’s provide a default reducer within our custom hook for users who don’t
need this feature.

// userExpanded.js
function useExpanded (
initialExpanded = false,
userReducer = (s, a) => a.internalChanges // 👈 default
) {
...
}

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.

The Final Result #


.Expandable-panel {
margin: 0;
padding: 1em 1.5em;
border: 1px solid hsl(216, 94%, 94%);;
min-height: 150px;
}

This has been such an interesting discourse!

Remember, the benefit of the state reducer pattern is 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, it may be a breaking change for the user.

Regardless, this is still such a good pattern for complex hooks and
components.

Quick Quiz! #

1
Why have we provided a default user reducer?

COMPLETED 0%
1 of 2

We’ve finally reached the end of this course! Congrats on making it this far 😃
Where to Go from Here?

Want to learn more about web development and React? I have a few other courses already on Educative! Have a
look.

WE'LL COVER THE FOLLOWING

• Other Courses by Me on Educative

Other Courses by Me on Educative #


Understanding Redux: A Beginner’s Guide To State Management — This is
a beginner’s gateway into the mechanics of using Redux as an application
building platform. It takes a bottom-up approach, enforcing the basic aspects
of Redux before incorporating them together in a creative and interactive
way. It begins with a short theoretical section before moving on to application-
based problems. The course does assume that the user has a basic idea of how
React works, as Redux works in the React environment.

Understanding Flexbox: Everything you need to know — This course will


cover all the fundamental and advanced concepts you need to be well versed
in the CSS Flexbox model. You will learn to lay out a Responsive Music App in
the process. It is a detailed course, and I hope you’re ready for it.

The Complete Advanced Guide to CSS — My goal is to take you from a


beginner (or intermediate) CSS user to one of the best CSS devs you know! The
course teaches the fundamentals of CSS, a guide to responsive design, sleek
interface creation with CSS animations, transitions, writing maintainable and
scalable CSS, and much more! Also, with this course you’ll get a private Slack
invite where you can ask me anything.

The 4 (Practical) Flexbox Tricks you Need to Know — If you’ve been trying
to get a grip on Flexbox - here’s an interactive practical tutorial for you. No
fluff, just the important practical tricks you need to know.
Last Words

Thank you for taking this course! Good luck in your future React endeavors!

WE'LL COVER THE FOLLOWING

• Last Words

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 practicing 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 a React engineer building highly reusable components
with advanced hook patterns.

Last Words #
Thank you for joining me on this journey on teaching Advanced React
Patterns with Hooks. As I sit in my room typing these last words with eyes
pale from working since midnight, I do hope that this course has helped you
learn one or two things. Heck! more than two things, I’d say.

If that’s the case, I couldn’t be any happier.

Do have a successful career, and practice what you’ve learnt here.

Cheers,

- Ohans Emmanuel

You might also like