Functional components with React Hooks. Why it's better?
When React.js 16.8 was released we got the opportunity to use React Hooks. Hooks make us able to write fully-functional components using functions. We can use all React.js features and do in in more convenient way.
A lot of people don't agree with Hooks conception. In this article I'd like to tell about some important advantages React Hooks give you and why we need to write with Hooks.
I will not talk about how to use hooks. It is not very important for the examples. If you want to read something on this topic, you can use official documentation. Also, if this topic will be interesting for you, I will write more about Hooks.
Hooks allow us to reuse our code easily
Let's imagine a component rendering a simple form. It can be something showing us a few inputs and allow us to change it's values.
With the class notation, there will be something like this:
class Form extends React.Component {
state = {
// Fields values
fields: {},
};
render() {
return (
);
};
}
Let's imagine now that we want to automatically save our fields values to a backend whenever they changed. I suggest to skip the definition of external functions like shallowEqual
and debounce
.
class Form extends React.Component {
constructor(props) {
super(props);
this.saveToDraft = debounce(500, this.saveToDraft);
};
state = {
// Fields values
fields: {},
// Draft saving meta
draft: {
isSaving: false,
lastSaved: null,
},
};
saveToDraft = (data) => {
if (this.state.isSaving) {
return;
}
this.setState({
isSaving: true,
});
makeSomeAPICall().then(() => {
this.setState({
isSaving: false,
lastSaved: new Date(),
})
});
}
componentDidUpdate(prevProps, prevState) {
if (!shallowEqual(prevState.fields, this.state.fields)) {
this.saveToDraft(this.state.fields);
}
}
render() {
return (
);
};
}
The same component with Hooks:
const Form = () => {
// Our state
const [fields, setFields] = useState({});
const [draftIsSaving, setDraftIsSaving] = useState(false);
const [draftLastSaved, setDraftLastSaved] = useState(false);
useEffect(() => {
const id = setTimeout(() => {
if (draftIsSaving) {
return;
}
setDraftIsSaving(true);
makeSomeAPICall().then(() => {
setDraftIsSaving(false);
setDraftLastSaved(new Date());
});
}, 500);
return () => clearTimeout(id);
}, [fields]);
return (
);
}
As we see, there is not a big difference here. We replaced this.state
with useState
hook and saving the draft in useEffect
hook now.
The difference I want to show here is (there is another differences too, but I'll concentrate on this one): we can easily extract this code from our component and use it somewhere else:
// useDraft hook can be used in any other component
const useDraft = (fields) => {
const [draftIsSaving, setDraftIsSaving] = useState(false);
const [draftLastSaved, setDraftLastSaved] = useState(false);
useEffect(() => {
const id = setTimeout(() => {
if (draftIsSaving) {
return;
}
setDraftIsSaving(true);
makeSomeAPICall().then(() => {
setDraftIsSaving(false);
setDraftLastSaved(new Date());
});
}, 500);
return () => clearTimeout(id);
}, [fields]);
return [draftIsSaving, draftLastSaved];
}
const Form = () => {
// Our state
const [fields, setFields] = useState({});
const [draftIsSaving, draftLastSaved] = useDraft(fields);
return (
);
}
And we can use useDraft
hook in other components! It is, of course, a very simple example, but code reusing is pretty important this and the example shows how easy it is with Hooks.
Hooks allow us to write component in more intuitive way
Let's imagine a class component rendering, for example, a chat screen, chats list and message form. Like this:
class ChatApp extends React.Component {
state = {
currentChat: null,
};
handleSubmit = (messageData) => {
makeSomeAPICall(SEND_URL, messageData)
.then(() => {
alert(`Message is sent to chat ${this.state.currentChat}`);
});
};
render() {
return (
{
this.setState({ currentChat });
}} />
);
};
}
Then imagine our user using this chat component:
- They open chat 1
- They send a message (let's imagine some slow network)
- They open chat 2
- They see an alert about theirs message:
- "Message is sent to chat 2"
But they're sent a message to the second chat, how did it happend? It was because the class method work with current value, not the value we had when we was starting a message request. It's not a big deal with a simple components like this, but it can be a source of bugs in more complex systems.
In the other hand, functional components acts in other way:
const ChatApp = () => {
const [currentChat, setCurrentChat] = useState(null);
const handleSubmit = useCallback(
(messageData) => {
makeSomeAPICall(SEND_URL, messageData)
.then(() => {
alert(`Message is sent to chat ${currentChat}`);
});
},
[currentChat]
);
render() {
return (
);
};
}
Let's imagine our user:
- They open chat 1
- They send a message (let's imagine some slow network)
- They open chat 2
- They see an alert about theirs message:
- "Message is sent to chat 1"
Well, what's changed? Now we are working with a value, captured in render moment. We're creating a new handleSubmit
every time currentChat
changed. It allow us to forget about future changes and think about now.
Every component render capturing everything it use.
Hooks make components lifecycle gone
This reason strongly intersects with the previous one. React is a declarative UI library. Declarativity makes UI creating and process way more easy. It allow us to forget about imperative DOM changes.
Even so, when we use classes, we face components lifecycle. It looks like this:
- Mounting
- Updating (whenever
state
orprops
changed) - Unmounting
It seems convinient but I convinced that it only because of our habits. It's not like React.
Instead of this, functional components allow us to write components' code and to forget about lifecycle. We think only about synchronization. We write the function makes our UI from input props and inner state.
At first useEffect
hook seems like replacement for componentDidMount
, componentDidUpdate
and other lifecycle methods. But it's not like this. When we use useEffect
we said to React: "Hey, make this after rendering my component".
Here is a good example from the big article about useEffect:
- React: Give me the UI when the state is
0
. - Your component:
- Here’s the render result:
.You clicked 0 times
- Also remember to run this effect after you’re done:
() => { document.title = 'You clicked 0 times' }
.
- Here’s the render result:
- React: Sure. Updating the UI. Hey browser, I’m adding some stuff to the DOM.
- Browser: Cool, I painted it to the screen.
- React: OK, now I’m going to run the effect you gave me.
- Running
() => { document.title = 'You clicked 0 times' }
.
- Running
It is way more declarative, isn't it?
In closing
React Hooks allow us to get rid of few problems and to make development easier. We just need to change our mental model. Functional component in fact is a function of UI from the props. They describe how all of it must be in any moment and helps us forget about changes.
Well, we need to learn how to use it, but hey, did you write a class components properly at the first time?