An opinion on the advantages of Hooks
React Hooks were introduced in React 16.8 which was released to the public on February 16, 2019. If we were to look at the Reactjs.org documents for a one-line explanation of what React Hooks are, we would be greeted with:
“ React Hooks let you use state and React lifecycle features without using class and React component lifecycle methods.”
But, they can be so much more than that…
They allow us to consider new design patterns, greater separation, and new ways to functionally program, giving us the chance to write code that's cleaner, easier to test and can approach problems from different angles.
It works quite happily alongside your existing React code and suits a gradual introduction to a platform or team. Hooks are completely opt-in and normal JS is perfectly backward compatible.
However, because of the nature of a developing environment, once you make a commitment to introducing it, other developers may be forced to pick it up during review or when revisiting code.
My personal thought…. I’m a big fan, there are no new concepts to learn per-say, it just wants to give us easier access to concepts we already know (props, state, context, refs, and lifecycle).
Why?
Reacts components and top-down data flow are great for breaking large UI into smaller reusable components. However, because components rely on stateful logic that can’t be extracted to separate concerns, it can create large components with duplicated logic, making things harder to test.
How Can We Improve This?
- Share reusable behaviors independent of component implementations (like the render prop pattern).
- Use state, Hook, and component lifecycle events without using a class.
- Use related logic in one place in your component, rather than splitting it between various lifecycle methods.
Let's take a closer look at the code above, in particular, the image on the far left. It contains a component that handles input fields with various states and lifecycle effects.
What if we wanted to share the logic here with another component? in other words, if we wanted to separate concerns.
Perhaps we could have a separately rendered JSX and then use either a render prop or a higher-order component pattern. Both work fine but both require a lot of changes to the code and are not always the easiest patterns to follow or to share between components.
If you now throw your glance to the far right of the image above…
Now we can see the power of being able to use state and React lifecycle events without creating a class. We can create one common way to reuse stateful logic between components.
It relies on functions that, when you think about it, make sense. Functions have been a perfect mechanism to separate concerns and pass logic for years. The magic is that our function can now also behave like a class and can manage both state and lifecycle events.
Let's look closer at two different examples.
You can see below that we have our usual React component. We create a class, use a constructor to create our state, add some of Reacts many lifecycle events, and manage the this
keyword by binding our methods in the constructor.
import React, { Component } from 'react';
import { render } from 'react-dom';
class App extends Component {
constructor() {
super();
this.buttonSubmit = React.createRef();
this.state = {
name: '',
username: '',
formSubmitted: false
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);}
componentDidMount(){
this.buttonSubmit.current.addEventListener('click', this.handleSubmit)
console.log("component mounted")
}
componentDidUpdate(){
console.log("component updated")
}
componentWillUnmount(){
this.buttonSubmit.current.EventListener('click', this.handleSubmit)
console.log("component unmounted")
}
handleChange(e){
const {name, value} = e.target
this.setState({...this.state, [name]: value})
}
handleSubmit(){
this.setState({formSubmitted: true})
}
render() {
return (
<div>
{this.state.formSubmitted
? <h1> {this.state.username} has signed in </h1>
: <div>
<input type="text"
name="name"
id="name"
placeholder="name"
value={this.state.name}
onChange={this.handleChange}
/>
<div>
<input type="text"
name="username"
id="username"
placeholder="username"
value={this.state.username}
onChange={this.handleChange}
/>
</div>
<button ref={this.buttonSubmit}>Submit</button>
</div>
}
</div>
);
}
}
render(<Apps />, document.getElementById('root'));
The example below shows us how this will look after we add some React Hook magic.
You’ll notice that we now manage state with one of React’s built-in Hooks called useState
, and we simplify all the lifeCycle
events with the useEffect
Hook.
import React, { Component, useState, useRef, useEffect} from 'react';
import { render } from 'react-dom';const useHandleForm = initialValue => {
const [value, setValue] = useState(initialValue)
const handleChange = (e) => {
const { value } = e.target
setValue(value)
}
return {
value,
onChange: handleChange
}
}
const App = () => {
const name = useHandleForm('');
const username = useHandleForm('');
const [formSubmitted, setFormSubmitted] = useState(false)
const buttonSubmit = useRef()
useEffect(() => {
// This gets called after every render, by default
// (the first one, and every one after that) buttonSubmit.current.addEventListener('click', handleSubmit) // If you want to implement componentWillUnmount,
// return a function from here, and React will call
// it prior to unmounting.
return () => buttonSubmit.current.removeEventListener('click', handleSubmit);}, [buttonSubmit])
const handleSubmit = () => {
setFormSubmitted(true);
}
return (
<div>
{this.state.formSubmitted
? <h1> {name.value} has signed in </h1>
: <div>
<input type="text"
name="name"
id="name"
placeholder="name"
{...name}
/>
<div>
<input type="text"
name="username"
id="username"
placeholder="username"
{...username}
/>
</div>
<button ref={this.buttonSubmit}>Submit</button>
</div>
}
</div>
);
}
render(<App />, document.getElementById('root'));
But there’s more…
We are not limited to the Hooks that React provides, we can also build our own and this is where the value of Hooks starts to shine for me.
In this situation, we are able to separate the logic that handles form inputs, we can now use this across the platform without relying on a render prop or higher-order component pattern.
There are repositories being created and updates as we speak, with helpful Hooks that we can re-use in the future. nikgraf hosts a great collection, and well worth a visit to familiarize yourself.
No more this
.
Another pain point when building class components is that we have to manage the this
keyword. React and ES6 have tried to ease this burden with multiple ways of dealing with the infinite this
.
Arrow functions have eased the burden by binding the this
keyword to the lexical scope rather than the context and the currently recommended way to deal with this
is to bind it in the constructor on creation as can be seen in our example.
But multiple solutions can add increased complexity among developers. React Hooks allow us to reduce our reliance on this
because useState
and useEffect
Hooks output local variables and eliminate the need to bind this
to the scope of the class.
Final Thoughts
We have only scratched the surface on React Hooks here. There are many more built-in Hooks that are worth discovering and are a worthwhile discussion for another time.
An example would be the useContext
Hook which can reduce the reliance on the Redux library. But what I’ve tried to do is give you a high-level opinion on the advantages.
I’m sure you have been reading articles across the web and you may have come to the conclusion that the adoption of React Hooks is gathering pace, as I have, with the team behind React donating considerable resources to it.
At the very least, it provides us with new tools and solutions to some common problems.
Leave a comment