The year 2018 brought a lot of new features into the React Ecosystem. The addition of these features is helping developers to focus more on user experience rather than spending time writing code logic.
It seems that React is investing more towards a functional programming paradigm, in a search of great tools for building a UI which is more robust and scalable.
At ReactConf October 2018, React announced a proposal API called Hooks which took the community by storm. Developers started exploring and doing experiments with them and it received great feedback in the RFC (Requests for comments). React 16.8.0 is the first release to support Hooks 🎉.
This article is my attempt to explain:
Why hooks were introduced
How can we prepare ourselves for this API
How can we write 90% cleaner code by using React Hooks 🎣
If you just want to have a feel of this new API first, I have created a demo to play with. Otherwise, let’s get started by looking at 3 major problems we are facing as of now:
1. Reusing Code Logic
You all know that reusing code logic is hard and it requires a fair bit of experience to get your head around. When I started learning React about two years ago, I used to create class components to encapsulate all my logic. And when it comes to sharing the logic across different components I would simply create a similarly looking component which would render a different UI. But that was not good. I was violating the DRY principle and ideally was not reusing the logic.
The Old Way
Slowly, I learned about HOC pattern which allowed me to use functional programming for reusing my code logic. HOC is nothing but a simple higher order function which takes another component(dumb) and returns a new enhanced component. This enhanced component will encapsulate your logic.
A higher order function is a function that takes a function as an argument, or returns a function.
export default function HOC(WrappedComponent){
return class EnhancedComponent extends Component {
/*
Encapsulate your logic here...
*/
// render the UI using Wrapped Component
render(){
return <WrappedComponent {...this.props} {...this.state} />
}
}
// You have to statically create your
// new Enchanced component before using it
const EnhancedComponent = HOC(someDumbComponent);
// And then use it as Normal component
<EnhancedComponent />
Then we moved into the trend of passing a function as props which marks the rise of the render props pattern. Render prop is a powerful pattern where “rendering controller” is in your hands. This facilitates the inversion of control(IoC) design principle. The React documentation describes it as a technique for sharing code between components using a prop whose value is a function.
A component with a render prop takes a function that returns a
React element and calls it instead of implementing its own render logic.
In simple words, you create a class component to encapsulate your logic (side effects) and when it comes to rendering, this component simply calls your function by passing only the data which is required to render the UI.
export default class RenderProps extends Component {
/*
Encapsulate your logic here...
*/
render(){
// call the functional props by passing the data required to render UI
return this.props.render(this.state);
}
}
// Use it to draw whatever UI you want. Control is in your hand (IoC)
<RenderProps render={data => <SomeUI {...data} /> } />
Even though both of these patterns were resolving the reusing code logic issues, they left us with a wrapper hell problem as shown below:
So, to sum it up we can see there are a few problems associated with reusing code logic:
- Not very intuitive to implement
- Lots of code
- Wrapper hell
2. Giant Components
Components are the basic unit of code reuse in React. When we have to abstract more than one behaviour into our class component, it tends to grow in size and becomes hard to maintain.
A class should have one and only one reason to change,
meaning that a class should have only one job.
By looking at the code example below we can deduce the following:
export default class GiantComponent extends Component {
componentDidMount(){
//side effects
this.makeRequest();
document.addEventListener('...');
this.timerId = startTimer();
// more ...
}
componentdidUpdate(prevProps){
// extra logic here
}
componentWillUnmount(){
// clear all the side effects
clearInterval(this.timerId);
document.removeEventListener('...');
this.cancelRequest();
}
render(){ return <UI />; }
- Code is spread across different life cycle hooks
- No single responsibility
- Hard to test
3. Classes are Hard for Humans and Machines
Looking at the human side of the problem, we all once tripped trying to call a function inside a child component and it says:
TypeError: Cannot read property 'setState' of undefined
and then scratched our head trying to figure out the cause: that you have forgotten to bind it in the constructor. So, this remains the topic of confusion even among some experienced developers.
this gets the value of object who invokes the function
Also, you need to write lots of boilerplate code to even start implementing the first side effect:
extends -> state -> componentDidMount -> componentWillUnmount -> render -> return
Classes are also hard for machines for the following reasons:
- Minified version won’t minify method names
- Unused methods won’t get stripped out
- Difficult with hot reloading and compiler optimisation
All the three problems we discussed above are not three distinct problems but these are symptoms of one single problem and that is React has no stateful primitive simpler than class component.
With the advent of the new React Hooks proposal API, we can solve this problem by abstracting our logic completely outside of our component. In fewer words, you can hook a stateful logic into the functional component.
React Hooks allow you to use state and other React features without writing a class.
Let’s see that in the code example below:
import React, { useState } from 'react';
export default function MouseTracker() {
// useState accepts initial state and you can use multiple useState call
const [mouseX, setMouseX] = useState(25);
const [mouseY, setMouseY] = useState(25);
return (
<div>
mouseX: {mouseX}, mouseY: {mouseY}
</div>
);
}
A call to useState hook returns a pair of values: the current state and a function that updates it. In our case, the current state value is mouseX and setter function is setMouseX. If you pass an argument to useState, that becomes the initial state of your component.
Now, the question is where do we call setMouseX. Calling it below the useState hook will cause an error. It will be the same as calling this.setState inside render function of class components.
So, the answer is that React also provides a placeholder hook called useEffect for performing all side effects.
import React, { useState } from 'react';
export default function MouseTracker() {
// useState accepts initial state and you can use multiple useState call
const [mouseX, setMouseX] = useState(25);
const [mouseY, setMouseY] = useState(25);
function handler(event) {
const { clientX, clientY } = event;
setMouseX(clientX);
setMouseY(clientY);
}
useEffect(() => {
// side effect
window.addEventListener('mousemove', handler);
// Every effect may return a function that cleans up after it
return () => window.removeEventListener('mousemove', handler);
}, []);
return (
<div>
mouseX: {mouseX}, mouseY: {mouseY}
</div>
);
}
This effect will be called both after the first render and after every update. You can also return an optional function which becomes a cleanup mechanism. This lets us keep the logic for adding and removing subscriptions close to each other.
The second argument to useEffect call is an optional array . Your effect will only re-run when the element value inside the array changes. Think of this as how shouldComponentUpdate works. If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This is close to the familiar mental model of componentDidMount and componentWillUnmount. If you want a deep dive into useEffect hook, I've written another article here.
But isn’t our MouseTracker component still holding the logic inside? What if another component wants to share mousemove behaviour as well? Also, adding one more effect (e.g window resize) would make it little hard to manage and we are back to the same problem as we saw in class components.
Now, the real magic is you can create your custom hooks outside of your function component. It is similar to keeping the logic abstracted to a separate module and share it across different components. Let see that in action.
// you can write your custom hooks in this file
import { useState, useEffect } from 'react';
export function useMouseLocation() {
const [mouseX, setMouseX] = useState(25);
const [mouseY, setMouseY] = useState(25);
function handler(event) {
const { clientX, clientY } = event;
setMouseX(clientX);
setMouseY(clientY);
}
useEffect(() => {
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
return [mouseX, mouseY];
}
And now we can clean up our MouseTracker component code (90%) to a newer version as shown below:
import React from 'react';
import { useMouseLocation } from 'customHooks.js';
export default function MouseTracker() {
// using our custom hook
const [mouseX, mouseY] = useMouseLocation();
return (
<div>
mouseX: {mouseX}, mouseY: {mouseY}
</div>
);
}
That’s kind of a “Eureka” moment! Isn’t it?
But before settling down and singing praises of React Hooks, let’s see what rules we should be aware of.
Rules of Hooks
- Only call hooks at the top level
- Can’t use hooks inside a class component
Explaining these rules are beyond the scope of this article. If you’re curious, I would recommend reading React docs and this article by Rudi Yardley.
React also has released an ESLint plugin called eslint-plugin-react-hooks that enforces these two rules. You can add this to your project by running:
# npm
npm install eslint-plugin-react-hooks --save-dev
# yarn
yarn add eslint-plugin-react-hooks --dev
This article was part of my talk at the ReactSydney meetup December 2018. I hope this article has intrigued you to give React hooks a try. I am super excited about the React roadmap which looks very promising and has the potential to change the way we use React currently.
You can find the source code and demo at this link.
If you liked the article, a few ❤️ will definitely make me smile 😀. There's more to come.
Top comments (14)
classes can be hard, but only without a proper type system; it's been interesting to see javascript development going round in circles saying types are not necessary and re-implementing over and over again on half-baked solutions
Personally, I feel that we should just let the JavaScript be how it is designed to be: Prototypal Inheritance and Functional Programming. No need to mimic the OOP mental modal as internally they are just syntactic sugar over functions.
i didn't say anything about OOP -- although, as FP, it provides good patterns and techniques that can be applied to javascript -- my point was about using classes in javascript and about it being "hard"; it is not, you just need a proper type system, as typescript; but react folks usually are not keen to the "overhead" and keep hitting their heads into the same walls
I think it is a bad practice to put a function inside a function since it will always be defined every render. I'm talking about the handler method in useMouseLocation custom hook.
Hi Lougie,
Thanks for your comment and I completely agree with you here 😍.
As Dan Abramov says in his article that each render has its own event handler. I know this is applicable when your event handler consumes some state or props variable so that you don't end up storing the stale value (closures).
But here for the sake of ingesting this new knowledge of using hooks, I was setting up an initial mindset for my readers and direct their attention towards using the side effect inside useEffect hook. I didn't want to set opinion or emphasize on some best patterns. That could be a completely separate article 😀
Thanks again for reading this article. 👏
Moving the
handler
function insideuseEffect
would fix this nicely (and in fact I see you already did this in the demo).Just when i thought React was the least horrible frontend framework, i read this. Why not a single VanillaJS file that registers an event listener to update its own properties?
Hi Cees,
I may be wrong but are you talking about Event-driven-programming? Or I may have not understood your question properly here, but handling events and updating dom properties can be a cumbersome task. What React gives you is a declarative approach. It's more of like
Moreover, React has its own implementation of a Data structure called Fiber which make sure to not block UI thread (main thread) and gives a performant UI. This is called the reconcilliation process.
I am pretty sure that handling UI updates via events only can be an assiduous task.
Thanks for your comment here 👏
Yes, for most programs a simple event handler that updates local properties should work fine.
I see your new code with hooks for 2 fields and it looks awkward... If you have 50 fields?
Then you either use your custom hook combining the relevant fields, or use useReducer.
useReducer with 50 cases? Custom hook is a solution, but its development is more difficult for developers. Who don't understand
this
, cannot design it. I already imagine a ton of terrible code from juniors in Internet... IMOSo with hooks available do you think it every makes sense to put logic in the view, or should all logic just go in a custom hook.
Carabiners resounded with a merry jingle, as a bunch of climbers flew by on the way down.