In this project, we will practice making higher-order components (HOC) to better understand this technique for reusing component logic. A higher-order component is a function that takes in a component as an argument and returns a new component.
cd
into the project directory.- Run
create-react-app ./
. - Remove the service worker from
src/index.js
:- Delete
import registerServiceWorker from './registerServiceWorker';
- Delete
registerServiceWorker();
- Delete
- Run
npm start
. - In a seperate terminal,
cd
into the project directory. - Create the following folders inside of
./src
- hocs
- components
We will create a high-order component that will check if a user is authenticated in order to render a component.
- Inside
./src/hocs
folder, create the following file:- withAuthentication.js
- Import
React
. React must be in scope when using jsx. - Create a function
withAuthentication
that takes in a component as a parameter calledWrappedComponent
.withAuthentication
should return another function that accepts aprops
parameter. - The inner function should return the
WrappedComponent
that was passed into thewithAuthentication
function ifprops.isAuthenticated
is true, andnull
if it is false. - With HOCs it is important that you pass along any props that will be passed to the component created from using the HOC, to the component that is passed into the HOC. You can do this using the spread operator.
- E.G.
<WrappedComponent {...props}>
- E.G.
./src/HOCs/withAuthentication.js
import React from 'react'
export default function withAuthentication(WrappedComponent) {
return function(props) {
return props.isAuthenticated ? <WrappedComponent { ...props } /> : null
}
}
- Inside the
./src/components
folder, create the following file:- SuperSecret.js
- Because this component is "super secret", we only want authenticated users to see it. So we will use the
withAuthentication
HOC to add logic to our component. - import
React
andwithAuthentication
. - create a functional component called
SuperSecret
that accepts aprops
argument and renders the followng:
(
<div style={{ margin: 20, border: '1px solid green'}}>
<h1>This is top secret!</h1>
<p>only an authenticated user can see this</p>
</div>
)
- Next, create a new component by invoking
withAuthentication
and passing inSuperSecret
. This new component will be theexport default
. - When we use this new component, we can pass a prop called
isAuthenticated
and if the value istrue
the component will render, if it isfalse
it will not.
./src/components/SuperSecret.js
import React from 'react'
import withAuthentication from '../HOCs/withAuthentication'
function SuperSecret(props) {
return (
<div style={{ margin: 20, border: '1px solid green'}}>
<h1>This is top secret!</h1>
<p>only an authenticated user can see this</p>
</div>
)
}
export default withAuthentication(SuperSecret)
- In
./src/App.js
, bring in our newly createdSuperSecret
component and add it to the jsx code in the render method. - Give it the prop
isAuthenticated
and set the value totrue
. In the browser, you should now see the secret component. - Next, set
isAuthenticated
to false, and you can see that the component no longer shows in the browser.
./src/App.js
import React, { Component } from 'react';
import './App.css';
import SuperSecret from './components/SuperSecret'
class App extends Component {
render() {
return (
<div className="App">
<SuperSecret isAuthenticated={true}/>
</div>
);
}
}
export default App;
Now we will create a higher-order component that will add toggle logic to a component. We can use this HOC with any component that needs to toggle something.
- Inside the
./src/hocs
folder, create the following file:- withToggle.js
- Import
React
andComponent
. - Create a function
withToggle
that takes in a component as it's parameterWrappedComponent
and returns a class componentWithToggle
.WithToggle
should have state with one propertytoggle
that is set tofalse
. WithToggle
should have a method,handleChange
, that will update the component's state and change the value oftoggle
to the opposite of its current value.- In the render method, create an object named
toggle
that will store thevalue
ofthis.state.toggle
, and thehandleChange
method. Then, return theWrappedComponent
and pass thetoggle
object as a prop. Make sure to also pass along the rest of the props{ ...props }
./src/HOCs/withToggle.js
import React, { Component } from 'react'
export default function (WrappedComponent) {
return class WithToggle extends Component {
state = {
toggle: false
}
handleChange = e => {
this.setState({
toggle: !this.state.toggle
})
}
render() {
let toggle = {
value: this.state.toggle,
handleChange: this.handleChange
}
return <WrappedComponent toggle={toggle} { ...this.props }/>
}
}
}
- Inside the
./src/components
folder, create the following files:- OnOffButton.js
- AccordianMenu.js
- In
OnOffButton.js
importReact
andwithToggle
. Then, create a functional componentOnOffButton
, that returns the following:( <button onClick={toggle.handleChange}> <h1>{ toggle.value ? 'ON' : 'OFF' }</h1> </button> )
- Create a new component by invoking
withToggle
and passing inOnOffButton
. This new component will be theexport default
. - In
AccordianMenu.js
importReact
andwithToggle
. Create a functional componentAccordianMenu
.function AccordianMenu(props) { let { toggle } = props return ( <div> <div id="title" style={styles.menuTitle} onClick={toggle.handleChange}> {props.title} </div> {toggle.value && <div id="body" style={styles.menuBody}>{props.children}</div> } </div> ) } let styles = { menuTitle: { border: '1px solid black', padding: 20 }, menuBody: { border: '1px solid black', borderTop: 'none', backgroundColor: '#F0F0F0', padding: 20 } }
- Let's walk through what is happening here. First, we are grabbing the toggle object from props which we will have access to once we pass
AccordianMenu
intowithToggle
. Then, we have a click event on the first childdiv
so when we click on it, thehandleChange
method, fromwithToggle
, will be invoked, and update the value oftoggle
. Below that, we are checking to see if the value oftoggle
istruthy
, and if it is, we render adiv
for the body. Notice that between the bodydiv
tags we are renderingprops.childred
.props.children
refers to the elements between the opening and closing tags of theAccordianMenu
component, when we use the component. - Create a new component by invoking
withToggle
and passing inAccordianMenu
. This new component will be theexport default
.
./src/components/OnOffButton.js
import React from 'react'
import withToggle from '../HOCs/withToggle'
function OnOffButton(props) {
let { toggle } = props
return (
<button style={styles.button} onClick={toggle.handleChange}>
<h1>{ toggle.value ? 'ON' : 'OFF' }</h1>
</button>
)
}
export default withToggle(OnOffButton)
let styles = {
button: {
border: '1px solid orange',
borderRadius: '3px',
padding: 20,
margin: 20
}
}
./src/components/AccordianMenu.js
import React from 'react'
import withToggle from '../HOCs/withToggle'
function AccordianMenu(props) {
let { toggle } = props
return (
<div>
<div style={styles.menuTitle} onClick={toggle.handleChange}>
{props.title}
</div>
{toggle.value && <div style={styles.menuBody}>{props.children}</div> }
</div>
)
}
export default withToggle(AccordianMenu )
let styles = {
menuTitle: {
border: '1px solid black',
padding: 20
},
menuBody: {
border: '1px solid black',
borderTop: 'none',
backgroundColor: '#F0F0F0',
padding: 20
}
}
- In App.js, bring in our newly created
OnOffButton
andAccordianMenu
components and add them to the jsx code in the render method.
./src/App.js
import React, { Component } from 'react';
import './App.css';
import SuperSecret from './components/SuperSecret'
import OnOffButton from './components/OnOffButton'
import AccordianMenu from './components/AccordianMenu'
class App extends Component {
render() {
return (
<div className="App">
<SuperSecret isAuthenticated={true}/>
<OnOffButton />
<AccordianMenu title="Aloha!" >
<p> this p tag is the "props.children" for the AccordianMenu component </p>
</AccordianMenu>
</div>
);
}
}
export default App;
Create these same higher-order components using render props instead.
If you see a problem or a typo, please fork, make the necessary changes, and create a pull request so we can review your changes and merge them into the master repo and branch.
© DevMountain LLC, 2018. Unauthorized use and/or duplication of this material without express and written permission from DevMountain, LLC is strictly prohibited. Excerpts and links may be used, provided that full and clear credit is given to DevMountain with appropriate and specific direction to the original content.