React Hooks Re-intro
The original post is in this repo
https://github.com/hayk94/react-hooks-intro-coffeeshop
where each branch has a readme.md explaining over the code.
This repository is for React Hooks introduction.
It consists of several numbered branches.
In each branch readme file goes over the code,
explaining the advantages and caveats.
Here in the master branch is a plain CRA app.
There are some additional configs for eslint using eslint-config-fbjs
with eslint-plugin-react-hooks.
And prettier config.
Disclaimer
Most stuff in this repo and posts have already been discussed,
by react team in the docs, by Dan Abramov in his amazing talk and blog,
and other people.
This repo is just a summation of my knowledge about hooks I gathered so far.
Just putting stuff in my own words.
Resources
https://reactjs.org/docs/hooks-intro.html
https://dev.to/dan_abramov/making-sense-of-react-hooks-2eib
https://overreacted.io/react-as-a-ui-runtime/
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
https://overreacted.io/how-are-function-components-different-from-classes/
https://overreacted.io/a-complete-guide-to-useeffect/
https://overreacted.io/writing-resilient-components/
App Intro
Imagine we are writing a coffeeshop menu app.
Users can choose the product they want and order it.
Good Old Classes
First we make aMenu.js
component.
import React, {Component} from 'react';
class Menu extends Component {
render() {
return (
<div>
Menu
</div>
);
}
}
export default Menu;
And make it render in our App.js
component.
import React, { Component } from 'react';
import './App.css';
import Menu from './Menu';
class App extends Component {
render() {
return (
<div className="App">
<section>
<Menu/>
</section>
</div>
);
}
}
export default App;
Then we add a select
to our menu app, with some products.
Add a button for ordering.
import React, {Component} from 'react';
class Menu extends Component {
render() {
return (
<div>
<b>Order: </b>
<select>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
<div>
<button>Order</button>
</div>
</div>
);
}
}
export default Menu;
Apparently this is what they sell at coffeeshops, isn't it?
Now we need some state for the selected product.
import React, {Component} from 'react';
class Menu extends Component {
constructor(props) {
super(props);
this.state = {
selected: 'Purple Haze',
};
}
render() {
return (
<div>
<b>Order: </b>
<select>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
<div>
<button>Order</button>
</div>
</div>
);
}
}
export default Menu;
Finally we need methods to select the product and order it.
import React, {Component} from 'react';
class Menu extends Component {
constructor(props) {
super(props);
this.state = {
selected: 'Purple Haze',
};
}
onChange(e) {
this.setState({selected: e.target.value});
}
onOrder() {
alert(`You ordered ${this.state.selected}`);
}
render() {
return (
<div>
<b>Order: </b>
<select onChange={this.onChange}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
<div>
<button onClick={this.onOrder}>Order</button>
</div>
</div>
);
}
}
export default Menu;
And here we made our app that's it.
Now lets go and order some stuff.
And...
Oops...
Right...
We get a big nice error message, guess what?
Turn to the next branch...
And now this!
So now we have an error! Or rather the error...
You know it right?
Let's fix it!
import React, {Component} from 'react';
class Menu extends Component {
constructor(props) {
super(props);
this.state = {
selected: 'Purple Haze',
};
this.onChange = this.onChange.bind(this);
this.onOrder = this.onOrder.bind(this);
}
onChange(e) {
this.setState({selected: e.target.value});
}
onOrder() {
alert(`You ordered ${this.state.selected}`);
}
render() {
return (
<div>
<b>Order: </b>
<select onChange={this.onChange}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
<div>
<button onClick={this.onOrder}>Order</button>
</div>
</div>
);
}
}
export default Menu;
We needed to bind
this
.
New Requirements
Suddenly we got new requirements from the client...
They want the page title to be the selected item of the user.
So we need something like this.
import React, {Component} from 'react';
class Menu extends Component {
constructor(props) {
super(props);
this.state = {
selected: 'Purple Haze',
};
this.onChange = this.onChange.bind(this);
this.onOrder = this.onOrder.bind(this);
}
componentDidUpdate() {
document.title = `Selected - ${this.state.selected}`;
}
onChange(e) {
this.setState({selected: e.target.value});
}
onOrder() {
alert(`You ordered ${this.state.selected}`);
}
render() {
return (
<div>
<b>Order: </b>
<select onChange={this.onChange}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
<div>
<button onClick={this.onOrder}>Order</button>
</div>
</div>
);
}
}
export default Menu;
Lifecycle methods yay!
Now we got it once user selects an item the document title changes accordingly.
Yet another requirement
And now we've been given another task based on the new requirements.
Users should be able to tell us how many products they want to order.
Pretty easy, right?
import React, {Component} from 'react';
class Menu extends Component {
constructor(props) {
super(props);
this.state = {
selected: 'Purple Haze',
count: 0,
};
this.onProductChange = this.onProductChange.bind(this);
this.onOrder = this.onOrder.bind(this);
this.onCountChange = this.onCountChange.bind(this);
}
componentDidUpdate() {
document.title = `Selected - ${this.state.selected}`;
}
onProductChange(e) {
this.setState({selected: e.target.value});
}
onOrder() {
alert(`You ordered ${this.state.count} ${this.state.selected}`);
}
onCountChange(e) {
this.setState({count: e.target.value});
}
render() {
return (
<div>
<div>
<b>Product: </b>
<select onChange={this.onProductChange}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
</div>
<div>
<b>Count: </b>
<input
type="number"
min={0}
value={this.state.count}
onChange={this.onCountChange}
/>
</div>
<div>
<button onClick={this.onOrder}>Order</button>
</div>
</div>
);
}
}
export default Menu;
We add a number input
, count
to our state, and an onCountChange
method.
Oh and right, we need to bind
this
.
Great we accomplished a lot today and feel proud.
Oh no, a bug was just reported
Whoops... We just barely finished the previous task,
yet a bug was reported from our previous feature.
They say when the users enter the page first time,
the page title doesn't show the selected product.
But it's not a bug! The user didn't select any product yet!
Oh really? It's a bug and you should fix it!
Anyway it needs to be done.
So after thinking a while, the best place for this would be componentDidMount
.
Yay another lifecycle method to the rescue!
import React, {Component} from 'react';
class Menu extends Component {
constructor(props) {
super(props);
this.state = {
selected: 'Purple Haze',
count: 0,
};
this.onProductChange = this.onProductChange.bind(this);
this.onOrder = this.onOrder.bind(this);
this.onCountChange = this.onCountChange.bind(this);
}
componentDidMount() {
document.title = `Selected - ${this.state.selected}`;
}
componentDidUpdate() {
document.title = `Selected - ${this.state.selected}`;
}
onProductChange(e) {
this.setState({selected: e.target.value});
}
onOrder() {
alert(`You ordered ${this.state.count} ${this.state.selected}`);
}
onCountChange(e) {
this.setState({count: e.target.value});
}
render() {
return (
<div>
<div>
<b>Product: </b>
<select onChange={this.onProductChange}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
</div>
<div>
<b>Count: </b>
<input
type="number"
min={0}
value={this.state.count}
onChange={this.onCountChange}
/>
</div>
<div>
<button onClick={this.onOrder}>Order</button>
</div>
</div>
);
}
}
export default Menu;
Phew... it's been a tough day, but we managed! Hooray!
Turn to the next branch...
App grows! Performance issues come up!
As our app grows, we are encountering some performance issues here and there.
To identify them we start using some debugging tools.
So you decide to put some loggers in your Menu.js
.
import React, {Component} from 'react';
class Menu extends Component {
constructor(props) {
super(props);
this.state = {
selected: 'Purple Haze',
count: 0,
};
this.onProductChange = this.onProductChange.bind(this);
this.onOrder = this.onOrder.bind(this);
this.onCountChange = this.onCountChange.bind(this);
}
componentDidMount() {
// eslint-disable-next-line
console.log('logger', this.state, this.props);
document.title = `Selected - ${this.state.selected}`;
}
componentDidUpdate() {
// eslint-disable-next-line
console.log('logger', this.state, this.props);
document.title = `Selected - ${this.state.selected}`;
}
onProductChange(e) {
this.setState({selected: e.target.value});
}
onOrder() {
alert(`You ordered ${this.state.count} ${this.state.selected}`);
}
onCountChange(e) {
this.setState({count: e.target.value});
}
render() {
return (
<div>
<div>
<b>Product: </b>
<select onChange={this.onProductChange}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
</div>
<div>
<b>Count: </b>
<input
type="number"
min={0}
value={this.state.count}
onChange={this.onCountChange}
/>
</div>
<div>
<button onClick={this.onOrder}>Order</button>
</div>
</div>
);
}
}
export default Menu;
So we put loggers in componentDidUpdate
and componentDidMount
.
Now you see the proper log after componentDidMount
.
You select a product and see the proper log in componentDidUpdate
, with new state and props.
You change the product count and see new state and props logged by componentDidUpdate
.
But wait a minute...
Doesn't that mean the document.title = newTitle
code executes on every update,
even though the selected product didn't change?
We need to fix that. And this logger tool is really helpful we should implement it for other components too in our app.
So maybe we fix the issue with an if
check. And make a HOC
for the logger.
As you are thinking about the solution a new high priority requirement arrives.
Turn to the next branch...
New Requirement with lots async stuff
So suddenly a new requirement arrives.
To implement it you added lots of async stuff to your on order function, so now it looks like this.
import React, {Component} from 'react';
class Menu extends Component {
constructor(props) {
super(props);
this.state = {
selected: 'Purple Haze',
count: 0,
};
this.onProductChange = this.onProductChange.bind(this);
this.onOrder = this.onOrder.bind(this);
this.onCountChange = this.onCountChange.bind(this);
}
componentDidMount() {
// eslint-disable-next-line
console.log('logger', this.state, this.props);
document.title = `Selected - ${this.state.selected}`;
}
componentDidUpdate() {
// eslint-disable-next-line
console.log('logger', this.state, this.props);
document.title = `Selected - ${this.state.selected}`;
}
onProductChange(e) {
this.setState({selected: e.target.value});
}
onOrder() {
setTimeout(() => {
alert(`You ordered ${this.state.count} ${this.state.selected}`);
}, 3000);
}
onCountChange(e) {
this.setState({count: e.target.value});
}
render() {
return (
<div>
<div>
<b>Product: </b>
<select onChange={this.onProductChange}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
</div>
<div>
<b>Count: </b>
<input
type="number"
min={0}
value={this.state.count}
onChange={this.onCountChange}
/>
</div>
<div>
<button onClick={this.onOrder}>Order</button>
</div>
</div>
);
}
}
export default Menu;
Everything seems to be working just fine, but then you get a bug report.
Wrong item is being ordered sometimes.
After trying to reproduce it for a while, you find the problem!
When you order a product then select another product before the order message appeared,
you get a wrong product in the message as it appears.
Hmmm...
Why that would happen? Nothing seems to be wrong with our code.
Turn to the next branch...
Ugh not this again!
So why that bug happens?
To give you a hint, lets take a look at the solution first.
import React, {Component} from 'react';
class Menu extends Component {
constructor(props) {
super(props);
this.state = {
selected: 'Purple Haze',
count: 0,
};
this.onProductChange = this.onProductChange.bind(this);
this.onOrder = this.onOrder.bind(this);
this.onCountChange = this.onCountChange.bind(this);
}
componentDidMount() {
// eslint-disable-next-line
console.log('logger', this.state, this.props);
document.title = `Selected - ${this.state.selected}`;
}
componentDidUpdate() {
// eslint-disable-next-line
console.log('logger', this.state, this.props);
document.title = `Selected - ${this.state.selected}`;
}
onProductChange(e) {
this.setState({selected: e.target.value});
}
onOrder() {
const {count, selected} = this.state;
setTimeout(() => {
alert(`You ordered ${count} ${selected}`);
}, 3000);
}
onCountChange(e) {
this.setState({count: e.target.value});
}
render() {
return (
<div>
<div>
<b>Product: </b>
<select onChange={this.onProductChange}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
</div>
<div>
<b>Count: </b>
<input
type="number"
min={0}
value={this.state.count}
onChange={this.onCountChange}
/>
</div>
<div>
<button onClick={this.onOrder}>Order</button>
</div>
</div>
);
}
}
export default Menu;
So we only changed this piece.
onOrder() {
const {count, selected} = this.state;
setTimeout(() => {
alert(`You ordered ${count} ${selected}`);
}, 3000);
}
Just one line of code fixes our issue.
Let's dive deep and understand what happens here.
The "this" is mutable
onOrder() {
setTimeout(() => {
alert(`You ordered ${this.state.count} ${this.state.selected}`);
}, 3000);
}
Let's take a look at this buggy code first.
We select a product - "GoGreen". State changes to it. Component re-renders.
this.state.selected === "GoGreen"
Click the order button. The onOrder
method fires.
setTimeout
starts. 3 seconds pass. The callback is executed.
We read this.state.selected
and get "GoGreen".
Here everything is working great. Now let's see how the bug happens.
We select a product - "Amnesia". State changes to it. Component re-renders.
this.state.selected === "Amnesia"
Click the order button. The onOrder
method fires.
setTimeout
starts. Before 3 seconds pass, we select another product - "GoGreen".
State changes to it. Component re-renders.
this.state.selected === "GoGreen"
3 seconds pass. setTimeout
callback runs. We read this.state.selected
and get "GoGreen".
However this time we clicked the order button when we selected the "Amnesia" product.
The problem here is the this
. It changes during the scope of the onOrder
.
Solution
Now let's take a look at the solution.
onOrder() {
const {count, selected} = this.state;
setTimeout(() => {
alert(`You ordered ${count} ${selected}`);
}, 3000);
}
We select a product - "Amnesia". State changes to it. Component re-renders.
this.state.selected === "Amnesia"
Click the order button. The onOrder
method fires.
We read the this.state.selected
and assign it to the new variable selected
in the function scope.
selected === "Amnesia"
setTimeout
starts. Before 3 seconds pass, we select another product - "GoGreen".
State changes to it. Component re-renders.
this.state.selected === "GoGreen"
3 seconds pass. setTimeout
callback runs. We read the selected
of the function scope not from the this
. And get "Amnesia".
The this
changed/mutated but function scope and variables in it were still the same.
So we solved the this
problem by function scope.
2 Dimensions
One way that I find easy to think about the this
and function scope, is to think about them like dimensions.
We have these 2 dimensions where we store our data.
The this
can change during the scope.
So you need to be aware of the 2 dimensions where our data reside. And how they interact.
Fucking bring hooks already!
Fine! Fine...
So you heard about this next hot thing that's called hooks.
Now you want to refactor your Menu.js
component to use hooks.
You were going to refactor it anyway because of performance issues in branch 3, so lets refactor straight to hooks.
Create a new simple functional component MenuFC.js
.
import React from 'react';
const MenuFc = () => {
return (
<div>
<div>
<b>Product: </b>
<select>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
</div>
<div>
<button>Order</button>
</div>
</div>
);
};
export default MenuFc;
So plain and beautiful. Simple function that returns some jsx.
Now what do you think, wouldn't it be nice if we could do something like this?
import React from 'react';
const MenuFc = () => {
const state = 'Purple Haze';
return (
<div>
<div>
<b>Product: </b>
<select value={state}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
</div>
<div>
<button>Order</button>
</div>
</div>
);
};
export default MenuFc;
Wow, our state to be a simple variable in function scope. That's crazy man!
But we need to somehow be able to change it, right? Otherwise it's not useful as state.
Imagine if we had setState
as simple function in scope.
<select onChange={setState} value={state}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
What do you think? It's so nice and clean.
So how do we accomplish this with hooks?
import React, {useState} from 'react';
const MenuFc = () => {
const [selected, setSelected] = useState('Purple Haze');
const onProductChange = (e) => {
setSelected(e.target.value);
};
return (
<div>
<div>
<b>Product: </b>
<select onChange={onProductChange}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
</div>
<div>
<button>Order</button>
</div>
</div>
);
};
export default MenuFc;
I know what you are thinking. "I liked you at first, but now, what kind of black magic is this?"
const [selected, setSelected] = useState('Purple Haze');
Please just give me a moment. It is a simple ES6 Array Destructuring.
You learnt the big and verbose class syntax, surely this tiny syntax won't hurt you.
Now let's look at benefits
Our selected
and setSelected
are just plain variables in our function scope.
useState
function imported from react
gets one argument - the initial state.
It returns an array.
The first element is the value of our state.
The second is a function to change the value.
We use that function to create a callback to execute when a user selects different product.
So take a look at this beauty once more.
import React, {useState} from 'react';
const MenuFc = () => {
const [selected, setSelected] = useState('Purple Haze');
const onProductChange = (e) => {
setSelected(e.target.value);
};
return (
<div>
<div>
<b>Product: </b>
<select value={selected} onChange={onProductChange}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
</div>
<div>
<button>Order</button>
</div>
</div>
);
};
export default MenuFc;
"But you are creating a new callback function on each render, it's bad for performance!".
Some of you might say.
Well it turns out... No.
Here is what react official docs say about that.
No. In modern browsers, the raw performance of closures compared to classes doesnโt differ significantly except in extreme scenarios
Moreover
Hooks avoid a lot of the overhead that classes require, like the cost of creating class instances and binding event handlers in the constructor.
So if we just separate out politics from these sentences, the raw meaning strictly equals
As React components classes do so much shit that if we just throw them away and use functions,
we get a lot of performance benefit compared to which creating a new callback function at every render at most cases is nothing
And you can find some performance tips and tricks mentioned there. We'll dive into these later.
Now let's add the ordering functionality.
import React, {useState} from 'react';
const MenuFc = () => {
const [selected, setSelected] = useState('Purple Haze');
const onProductChange = (e) => {
setSelected(e.target.value);
};
const onOrder = () => {
setTimeout(() => {
alert(`You ordered ${count} ${selected}`);
}, 3000);
};
return (
<div>
<div>
<b>Product: </b>
<select value={selected} onChange={onProductChange}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
</div>
<div>
<button onClick={onOrder}>Order</button>
</div>
</div>
);
};
export default MenuFc;
Again. Simple as that, just a plain function.
Notice in this case we don't need to put the onOrder
in the component scope.
We can declare it outside and use it inside the component by passing arguments.
const onOrder = (count, selected) => {
setTimeout(() => {
alert(`You ordered ${count} ${selected}`);
}, 3000);
};
Here is another great thing about hooks. You can have as many of them as you'd like.
Let's implement the count.
import React, {useState} from 'react';
const MenuFc = () => {
const [selected, setSelected] = useState('Purple Haze');
const onProductChange = (e) => {
setSelected(e.target.value);
};
const [count, setCount] = useState(0);
const onCountChange = (e) => {
setCount(e.target.value);
};
const onOrder = () => {
setTimeout(() => {
alert(`You ordered ${count} ${selected}`);
}, 3000);
};
return (
<div>
<div>
<b>Product: </b>
<select onChange={onProductChange}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
</div>
<div>
<b>Count: </b>
<input
type="number"
min={0}
value={count}
onChange={onCountChange}
/>
</div>
<div>
<button onClick={onOrder}>Order</button>
</div>
</div>
);
};
export default MenuFc;
What a beauty! And we don't have any this
problems.
No bindings! No async bugs! Now it's just a plain function and only a scope.
Now let's get acquainted with the next hook.
Turn to the next branch.
Side effects!
Where did we perform side effects in class components?
Lifecycle methods.
To perform side effects in functional components now we have useEffect
.
In our Menu.js
class component we have a side effect for changing our document title.
Here is how we do it in MenuFC.js
import React, {useState, useEffect} from 'react';
const MenuFc = () => {
const [selected, setSelected] = useState('Purple Haze');
const onProductChange = e => {
setSelected(e.target.value);
};
const [count, setCount] = useState(0);
const onCountChange = e => {
setCount(e.target.value);
};
const onOrder = () => {
setTimeout(() => {
alert(`You ordered ${count} ${selected}`);
}, 3000);
};
useEffect(() => {
document.title = `Selected - ${selected}`;
});
return (
<div>
<div>
<b>Product: </b>
<select onChange={onProductChange}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
</div>
<div>
<b>Count: </b>
<input type="number" min={0} value={count} onChange={onCountChange} />
</div>
<div>
<button onClick={onOrder}>Order</button>
</div>
</div>
);
};
export default MenuFc;
useEffect
accepts a function as an argument.
There we perform our side effects.
It runs on every render.
It behaves
as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
https://reactjs.org/docs/hooks-effect.html
We also had a logger in our class component. Let's add it.
import React, {useState, useEffect} from 'react';
const MenuFc = () => {
const [selected, setSelected] = useState('Purple Haze');
const onProductChange = e => {
setSelected(e.target.value);
};
const [count, setCount] = useState(0);
const onCountChange = e => {
setCount(e.target.value);
};
const onOrder = () => {
setTimeout(() => {
alert(`You ordered ${count} ${selected}`);
}, 3000);
};
useEffect(() => {
// eslint-disable-next-line
console.log('logger', selected, count);
document.title = `Selected - ${selected}`;
});
return (
<div>
<div>
<b>Product: </b>
<select onChange={onProductChange}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
</div>
<div>
<b>Count: </b>
<input type="number" min={0} value={count} onChange={onCountChange} />
</div>
<div>
<button onClick={onOrder}>Order</button>
</div>
</div>
);
};
export default MenuFc;
Everything looks great right? Before we go deeper one thing...
useEffect is a whole new concept
Do not think of useEffect
as a new lifecycle method.
It's a whole new concept.
It's not a lifecycle method, it behaves similar to them.
useEffect
timing is different.
https://reactjs.org/docs/hooks-reference.html#timing-of-effects
Old problems
In our class component we had a few issues.
As state changes our component re-renders.
componentDidUpdate
fires and document.title = this.state.selected
was running,
even though we changed only the count and not the title. With classes we'd put some if check.
Also we wanted to make our logger functionality reusable. With classes we'd make a HOC.
The same problems we have now here with our useEffect
hook.
At the moment it's as bad as lifecycle methods.
Let's see how it's actually better.
Turn to the next branch.
It's useEffect not effects
Take a look at this code.
useEffect(() => {
// eslint-disable-next-line
console.log('logger', selected, count);
document.title = `Selected - ${selected}`;
});
While it may seem okay, conceptually it is not.
In the callback function, the 2 lines of code have different concerns.
They do stuff unrelated to each other.
We need better separation of concerns.
So in fact this is much more better.
import React, {useState, useEffect} from 'react';
const MenuFc = () => {
const [selected, setSelected] = useState('Purple Haze');
const onProductChange = e => {
setSelected(e.target.value);
};
const [count, setCount] = useState(0);
const onCountChange = e => {
setCount(e.target.value);
};
const onOrder = () => {
setTimeout(() => {
alert(`You ordered ${count} ${selected}`);
}, 3000);
};
useEffect(() => {
document.title = `Selected - ${selected}`;
});
useEffect(() => {
// eslint-disable-next-line
console.log('logger', selected, count);
});
return (
<div>
<div>
<b>Product: </b>
<select onChange={onProductChange}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
</div>
<div>
<b>Count: </b>
<input type="number" min={0} value={count} onChange={onCountChange} />
</div>
<div>
<button onClick={onOrder}>Order</button>
</div>
</div>
);
};
export default MenuFc;
Deps
Now we need our document.title
effect run only when the selected
state changed.
In usual componentDidUpdate
you'd do some prevProps
comparisons and so on.
Guess what? useEffect
now will do it for you! You just need to tell it what it needs to check.
How do we tell it what variables to check? Just pass a second argument to it.
import React, {useState, useEffect} from 'react';
const MenuFc = () => {
const [selected, setSelected] = useState('Purple Haze');
const onProductChange = e => {
setSelected(e.target.value);
};
const [count, setCount] = useState(0);
const onCountChange = e => {
setCount(e.target.value);
};
const onOrder = () => {
setTimeout(() => {
alert(`You ordered ${count} ${selected}`);
}, 3000);
};
useEffect(() => {
document.title = `Selected - ${selected}`;
});
useEffect(() => {
// eslint-disable-next-line
console.log('logger', selected, count);
});
return (
<div>
<div>
<b>Product: </b>
<select onChange={onProductChange}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
</div>
<div>
<b>Count: </b>
<input type="number" min={0} value={count} onChange={onCountChange} />
</div>
<div>
<button onClick={onOrder}>Order</button>
</div>
</div>
);
};
export default MenuFc;
Yes simple like that. useEffect
gets second argument. It's an array.
In that array you put any variable which change should trigger the effect.
In case those variables don't change. The effect will not run.
Just add another console.log
to that effect and you will see it now runs only when the selected
changes.
Re-usable hooks
Let's dive right into coding.
We create a new hooker.js
file.
import {useEffect} from 'react';
// updateDocumentTitle name is bad the custom hook name should start with "use"
export const useDocumentTittle = title => {
useEffect(() => {
document.title = title;
}, [title]);
};
export const useLogger = (...args) => {
useEffect(() => {
// eslint-disable-next-line
console.log('logger', ...args);
});
};
Then we do this in our MenuFC.js
import React, {useState} from 'react';
import {useDocumentTittle, useLogger} from './hooker';
const MenuFc = () => {
const [selected, setSelected] = useState('Purple Haze');
const onProductChange = e => {
setSelected(e.target.value);
};
const [count, setCount] = useState(0);
const onCountChange = e => {
setCount(e.target.value);
};
const onOrder = () => {
setTimeout(() => {
alert(`You ordered ${count} ${selected}`);
}, 3000);
};
useDocumentTittle(`Selected - ${selected}`);
useLogger(selected, count);
return (
<div>
<div>
<b>Product: </b>
<select onChange={onProductChange}>
<option value="Purple Haze">Purple Haze</option>
<option value="Amnesia">Amnesia</option>
<option value="GoGreen">GoGreen</option>
</select>
</div>
<div>
<b>Count: </b>
<input type="number" min={0} value={count} onChange={onCountChange} />
</div>
<div>
<button onClick={onOrder}>Order</button>
</div>
</div>
);
};
export default MenuFc;
That's right!
Now we can use any of these hooks in any React Functional Component.
That's how simple sharing logic can be.
Some general rules for hooks
Remember that custom hooks should be named as native hooks. The use word should be used.
Hooks cannot be in conditions. But you can have conditions in your useEffect
callback
And so on... You definitely should check eslint-plugin-react-hooks
To be continued...
And that's it for hooks intro!
Soon I'll dive deeper into more specific cases...
So keep up with me!
See you soon!
Top comments (1)
Many early birds have already started using this custom hooks library
in their ReactJs/NextJs project.
Have you started using it?
scriptkavi/hooks
PS: Don't be a late bloomer :P