A Higher Order Component (HOC) is an advanced React JS pattern that allows us the capability of reusing component logic. Check out the HOC Docs.
This paradigm allows us developers the ability to encapsulate some shared functionality between components and avoid repeating ourselves. An HOC takes in a component as an argument, adds some functionality to that supplied component and returns it.
Higher Order Component Explained
For the exercise, we'll simply start by creating a ClickButton component that gets passed a function and a count from state using props.
// index.js
import React from "react";
import ReactDOM from "react-dom";
import ClickButton from "./components/Buttons/ClickButton";
class App extends React.Component {
constructor() {
super();
this.state = {
count: 0
}
}
incrementCount = () => {
this.setState(prevState => {
return {
count: prevState.count + 1
};
});
};
render() {
return (
<div className="App">
<ClickButton incrementCount={this.incrementCount} count={this.state.count} />
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"););
// /Buttons/ClickButton.js
import React from "react";
const ClickButton = props => {
return (
<button onClick={props.incrementCount}>Plus one: {props.count}</button>
);
};
export default ClickButton;
Let's say we want to extend the functionality from one component to another while keeping it in its seperate context:
ButtonGenerator
component in /components/HOC/ButtonGenerator.js
ButtonGenerator
will be a functional component that takes in a Button as an argument: const ButtonGenerator = Button =>
class extends React.Component {...
where state will be held export default ButtonGenerator;
import React from "react";
const ButtonGenerator = Button => // Step 2
class extends React.Component { // Step 3
constructor() {
super();
this.state = { // Step 1 and 4
count: 0
};
}
incrementCount = () => { // Step 1 and 4
this.setState(prevState => {
return {
count: prevState.count + 1
};
});
};
render() {
return (
<Button incrementCount={this.incrementCount} count={this.state.count} /> // Step 5
);
}
};
export default ButtonGenerator; // Step 6
index.js
no longer holds the state. Instead, we pass in the ButtonGenerator
and create a wrapped component called HOCClickButton
.
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
import ClickButton from "./components/Buttons/ClickButton";
import ButtonGenerator from "./components/HOC/ButtonGenerator";
const HOCClickButton = ButtonGenerator(ClickButton);
class App extends React.Component {
render() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<HOCClickButton />
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Here are the basic building blocks to create another button
// From index.js
import DoubleClickButton from "./components/Buttons/DoubleClickButton";
const HOCDoubleClickButton = ButtonGenerator(DoubleClickButton);
<HOCDoubleClickButton />
// DoubleClickButton.js
import React from "react";
const DoubleClickButton = props => {
return (
<button onDoubleClick={props.incrementCount}>
Double Click Button {props.count}
</button>
);
};
export default DoubleClickButton;
We can use the same logic to create a higher order authenticate component that will wrap our app and conditionally render a login form or the app itself.
// index.js
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));
// App.js
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
import ClickButton from "./components/Buttons/ClickButton";
import DoubleClickButton from "./components/Buttons/DoubleClickButton";
import ButtonGenerator from "./components/HOC/ButtonGenerator";
const HOCClickButton = ButtonGenerator(ClickButton);
const HOCDoubleClickButton = ButtonGenerator(DoubleClickButton);
class App extends React.Component {
render() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<HOCClickButton />
<HOCDoubleClickButton />
</div>
);
}
}
export default App;
Create a <AppPage />
in your root directory.
// AppPage.js
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
import ClickButton from "./components/Buttons/ClickButton";
import DoubleClickButton from "./components/Buttons/DoubleClickButton";
import ButtonGenerator from "./components/HOC/ButtonGenerator";
const HOCClickButton = ButtonGenerator(ClickButton);
const HOCDoubleClickButton = ButtonGenerator(DoubleClickButton);
class AppPage extends React.Component {
render() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<HOCClickButton />
<HOCDoubleClickButton />
</div>
);
}
}
export default AppPage;
// App.js
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
import AppPage from "./AppPage";
class App extends React.Component {
render() {
return (
<div>
<AppPage />
</div>
);
}
}
export default App;
Building the Higher Order Component
Authentication.js
file in the /HOC
folder.Authenticate
component should be able to take in another component as an argument, and it will return an anonymous class component.export default Authenticate;
.// Authenticate.js
import React from "react";
const Authenticate = App =>
class extends React.Component {
render() {
return <App />;
}
};
export default Authenticate;
// App.js
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
import AppPage from "./AppPage";
import Authenticate from "./components/HOC/Authenticate";
class App extends React.Component {
render() {
return (
<div>
<AppPage />
</div>
);
}
}
export default Authenticate(App);
Build out the LoginPage component. You can design it how you like
import React, { Component } from "react";
class Login extends Component {
constructor(props) {
super(props);
this.state = {
username: "",
password: ""
};
}
handleInputChange = e => {
this.setState({ [e.target.name]: e.target.value });
};
handleLoginSubmit = e => {
const user = this.state.username;
localStorage.setItem("user", user);
window.location.reload();
};
render() {
return (
<form className="login-form">
<h3>Login</h3>
<input
type="text"
placeholder="Username"
name="username"
value={this.state.username}
onChange={this.handleInputChange}
/>
<input
type="password"
placeholder="Password"
name="password"
value={this.state.password}
onChange={this.handleInputChange}
/>
<br />
<br />
<button onClick={this.handleLoginSubmit}>
Log In
</button>
</form>
);
}
}
export default Login;
Extending the functionality of the Authenticate higher order component to conditionally render Login or App
isLoggedIn
boolean flag and set it to falseisLoggedIn
<App />
, else we will return the <Login />
import React from "react";
const Authenticate = App =>
class extends React.Component {
render() {
return <App />;
}
};
export default Authenticate;
// becomes
import React from "react";
import Login from "../Login/Login";
const Authenticate = App =>
class extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoggedIn: false
};
}
componentDidMount() {
if (!localStorage.getItem("user")) {
this.setState({ isLoggedIn: false });
} else {
this.setState({ isLoggedIn: true });
}
}
render() {
if (this.state.isLoggedIn) return <App />;
return <Login />;
}
};
export default Authenticate;