Prerequisite: Basic knowledge about React
This article is intended to give you an understanding of how React Hooks helps us to share common logic between components and what differences between using Hooks and other methods.
Take a look if you have not read the Part I: Complicated lifecycles
You spent 2 hours writing a beautiful functionality in your component and then just a minute later, your Boss wants the same thing … but in another component. What should you do now?
😅 Easy, I will do some copy + paste stuff
As a React developer, you will at some point run into the situation where you have to share some common logic between components. Higher-order components (HOCs) are well known in the React community for solving that kind of issue. However, I found HOCs a little bit complicated for beginners to get the idea while Hooks makes it way much easier and cleaner.
💪 Let’s get started!
Our task today is to help Carey to teach her naughty twins, Zack and Cody, she will shout at them if they do something bad many times
- Zack.jsx
class Zack extends React.Component {
state = {
numberOfBadActions: 0
}
componentDidUpdate(prevProps, prevState) {
if (
prevState.numberOfBadActions !== this.state.numberOfBadActions &&
this.state.numberOfBadActions === 3
) {
console.log('Use your head!')
this.setState({numberOfBadActions: 0})
}
}
doBadAction = () => {
this.setState(state => ({numberOfBadActions: state.numberOfBadActions + 1}))
}
render() {
return (
<div>
<p>Number of bad actions: {this.state.numberOfBadActions}</p>
<button onClick={this.doBadAction}>Do bad action</button>
</div>
)
}
}
export default Zack
Look at Cody, I have to shout at him, too! 😫
0. Copy and paste
I’ve just copied Zack.jsx and rename the file to Cody.jsx and also change the component name to Cody
- Cody.jsx
class Cody extends React.Component {
state = {
numberOfBadActions: 0
}
componentDidUpdate(prevProps, prevState) {
if (
prevState.numberOfBadActions !== this.state.numberOfBadActions &&
this.state.numberOfBadActions === 3
) {
console.log('Use your head!')
this.setState({numberOfBadActions: 0})
}
}
doBadAction = () => {
this.setState(state => ({numberOfBadActions: state.numberOfBadActions + 1}))
}
render() {
return (
<div>
<p>Number of bad actions: {this.state.numberOfBadActions}</p>
<button onClick={this.doBadAction}>Do bad action</button>
</div>
)
}
}
export default Cody
I feel like patting myself on the back for solving this in just 1 second 😎
It looks good for now until later **Carey** decides to change the method of teaching her sons. In that case, we end up updating **2 components** at the same time and could you imagine what if she has to do the same thing with her sons’ friends, too. That’s tricky because we have to copy and paste the code everywhere and the hard part is that we have to update all these components if something related to that logic needs to be changed 😖
1. Higher-Order Component
A higher-order component is a function that takes a component and returns a new component
In our case, all you need to do is to take all the teaching-related things outside the React component and move it into a HOC
export const withMom = (WrappedComponent) => {
return class WithMom extends React.Component {
state = {
numberOfBadActions: 0
}
componentDidUpdate(prevProps, prevState) {
if (
prevState.numberOfBadActions !== this.state.numberOfBadActions &&
this.state.numberOfBadActions === 3
) {
console.log('Use your head!')
this.setState({numberOfBadActions: 0})
}
}
doBadAction = () => {
this.setState(state => ({numberOfBadActions: state.numberOfBadActions + 1}))
}
render() {
return (
<WrappedComponent
numberOfBadActions={this.state.numberOfBadActions}
doBadAction={this.doBadAction}
{...this.props}/>
)
}
}
}
withMom HOC is a function that accepts one argument as Component and returns a new and enhanced Component with all logics related to teaching. Now you can use withMom HOC to wrap components like below 👇
• Zack.jsx
class Zack extends React.Component {
render() {
return (
<div>
<p>Number of bad actions: {this.props.numberOfBadActions}</p>
<button onClick={this.props.doBadAction}>Do bad action</button>
</div>
)
}
}
export default withMom(Zack)
- Cody.jsx
class Cody extends React.Component {
render() {
return (
<div>
<p>Number of bad actions: {this.props.numberOfBadActions}</p>
<button onClick={this.props.doBadAction}>Do bad action</button>
</div>
)
}
}
export default withMom(Cody)
HOC helps you to organize your code in a much better way. In our case, Zack and Cody components do not care about the teaching logic anymore because now the withMom HOC encapsulates that logic and passes it down to the wrapped component. And the amazing part is that if Carey wants to change her method, all we need to do is to tweak the code in only 1 place — withMom HOC.
😓 Unfortunately, there is a major downside to using HOCs. Imagine that you have more than one HOC to consume then you start to face up with the problem of controlling all the passing down props and Wrapper Hell issue
export default withGrandPa(
withDad(
withMom(
Cody
)
)
)
Then our DOM looks like this
<WithGrandPa>
<WithDad>
<WithMom>
<Cody/>
</WithMom>
</WithDad>
</WithGrandPa>
Normal people: Wrapper Hell
Me as an intellectual: My Asian family clean architecture 👪
2. React Hooks
🔥 It would be a long story here, stay with me and I will walk you through step by step.
If you have not read the part I of this series, I recommend you read it first then come back otherwise you may get lost
• Step 1: Convert the Zack component to a function component
const Zack = () => {
const [numberOfBadActions, setNumberOfBadActions] = React.useState(0)
React.useEffect(() => {
if (numberOfBadActions === 3) {
console.log('Use your head!')
setNumberOfBadActions(0)
}
}, [numberOfBadActions])
const doBadAction = () => {
setNumberOfBadActions(numberOfBadActions => numberOfBadActions + 1)
}
return (
<div>
<p>Number of bad actions: {numberOfBadActions}</p>
<button onClick={doBadAction}>Do bad action</button>
</div>
)
}
export default Zack
• Step 2: Write custom Hook
A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.
const useYourHead = (initialNumberOfBadActions) => {
const [numberOfBadActions, setNumberOfBadActions] = React.useState(initialNumberOfBadActions)
React.useEffect(() => {
if (numberOfBadActions === 3) {
console.log('Use your head!')
setNumberOfBadActions(0)
}
}, [numberOfBadActions])
const doBadAction = () => {
setNumberOfBadActions(numberOfBadActions => numberOfBadActions + 1)
}
return [numberOfBadActions, doBadAction]
}
When we need to share logic between 2 functions, we extract the logic to a third function. The same thing is applied when we want to share logic between React components because they are functions and Hook is also a function.
I’ve just extracted all the code that we want to share to a custom Hook named useYourHead. We need to think about what arguments this function should accept and what it should return as custom Hook is just a normal function. In our case, useYourHead accepts the initial number of bad actions and return numberOfBadActions as well as doBadAction
- Step 3: Use our custom Hook
Zack.jsx
const Zack = () => {
const [numberOfBadActions, doBadAction] = useYourHead(0)
return (
<div>
<p>Number of bad actions: {numberOfBadActions}</p>
<button onClick={doBadAction}>Do bad action</button>
</div>
)
}
export default Zack
Cody.jsx
const Cody = () => {
const [numberOfBadActions, doBadAction] = useYourHead(0)
return (
<div>
<p>Number of bad actions: {numberOfBadActions}</p>
<button onClick={doBadAction}>Do bad action</button>
</div>
)
}
export default Cody
3. Conclusion:
🚀 Hooks help us to inject reusable logic to React components without creating HOCs. As you can see, we don’t have to deal with the issue of Wrapper Hell or the problem of passing down props through many component layers. 👏👏👏
Here are some good resources for you:
• React HOCs
• Custom Hooks
• Modern React workshop by Kent C. Dodds (Part I)
• Modern React workshop by Kent C. Dodds (Part II)
🙏 💪 Thanks for reading!
Please leave your comments below to let me know what do you think about this article