A relatively recently released version of React.js 16.8, with which we have become available hooks. The concept of hooks allows you to write full-fledged functional components using all the features of React, and allows you to do this in many ways more conveniently than we did with the help of classes.
Many have perceived the appearance of criticized hooks, and in this article I would like to talk about some of the important advantages that functional components with hooks give us, and why we should go for them.
I will not deliberately go into the details of using hooks. This is not very important for understanding the examples in this article, a rather general understanding of the work of React. If you want to read on this topic, information about hooks is in the documentation , and if this topic is interesting, I will write an article in more detail about when, which ones, and how to use hooks correctly.
Let's imagine a component that renders a simple form. Something that just displays several inputs and allows us to edit them.
Something like this, if you simplify a lot, this component would look like a class:
class Form extends React.Component { state = { // fields: {}, }; render() { return ( <form> {/* */} </form> ); }; }
Now imagine that we want to automatically save the field values when they change. I propose to omit the announcement of additional functions, such as shallowEqual
and debounce
.
class Form extends React.Component { constructor(props) { super(props); this.saveToDraft = debounce(500, this.saveToDraft); }; state = { // fields: {}, // , 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 ( <form> {/* , */} {/* */} </form> ); }; }
The same example, but with hooks:
const Form = () => { // 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 ( <form> {/* , */} {/* */} </form> ); }
As we can see, the difference is not very big. We changed the useState
on the useState
hook and call saving to a draft not in componentDidUpdate
, but after rendering the component using the useEffect
hook.
The difference that I want to show here (there are others, about them will be lower): we can render this code and use it in another place:
// useDraft 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 = () => { // const [fields, setFields] = useState({}); const [draftIsSaving, draftLastSaved] = useDraft(fields); return ( <form> {/* , */} {/* */} </form> ); }
Now we can use the useDraft
hook that we just wrote in other components! This, of course, is a very simplified example, but reusing a functional of the same type is a very useful feature.
Imagine a component (while in the form of a class), which, for example, displays the current chat window, the list of possible recipients and the form for sending a message. Something like this:
class ChatApp extends React.Component { state = { currentChat: null, }; handleSubmit = (messageData) => { makeSomeAPICall(SEND_URL, messageData) .then(() => { alert(` ${this.state.currentChat} `); }); }; render() { return ( <Fragment> <ChatsList changeChat={currentChat => { this.setState({ currentChat }); }} /> <CurrentChat id={currentChat} /> <MessageForm onSubmit={this.handleSubmit} /> </Fragment> ); }; }
The example is very conditional, but quite suitable for the demonstration. Provide these user actions:
But did the message go to chat 1? This happened because the class method did not work with the value that was at the time of sending, but with the one that was already at the time of the completion of the request. This would not be a problem in such a simple case, but correcting such behavior will, firstly, require additional care and additional processing, and secondly, it can be a source of bugs.
In the case of a functional component, the behavior is different:
const ChatApp = () => { const [currentChat, setCurrentChat] = useState(null); const handleSubmit = useCallback( (messageData) => { makeSomeAPICall(SEND_URL, messageData) .then(() => { alert(` ${currentChat} `); }); }, [currentChat] ); render() { return ( <Fragment> <ChatsList changeChat={setCurrentChat} /> <CurrentChat id={currentChat} /> <MessageForm onSubmit={handleSubmit} /> </Fragment> ); }; }
Submit the same user actions:
So what has changed? What has changed is that now for each render, for which currentChat
is different, we are creating a new method. This allows us not to think at all about whether something will change in the future - we work with what we have now . Each render component closes in itself everything that applies to it .
This item strongly intersects with the previous one. React is a library for declarative interface description. Declarativity greatly facilitates the writing and maintenance of components, allows us to think less about what would need to be done imperative if we did not use React.
Despite this, when using classes, we are confronted with the life cycle of a component. If you do not go deep, it looks like this:
state
or props
)It seems convenient, but I am convinced that it is convenient solely because of familiarity. This approach is not like React.
Instead, functional components with hooks allow us to write components, thinking not about the life cycle, but about synchronization . We write a function so that its result uniquely reflects the state of the interface, depending on the external parameters and the internal state.
The useEffect
, which is perceived by many as a direct replacement for componentDidMount
, componentDidUpdate
and so on, is actually intended for another. When using it, we kind of say to the reactor: "After you render it, please complete these effects."
Here is a good example of how a component works with a click counter from a large article on useEffect :
<p> 0 </p>
.() => { document.title = ' 0 ' }
.() => { document.title = ' 0 ' }
Much more declarative, isn't it?
React Hooks allow us to get rid of some problems and facilitate the perception and writing of component code. You just need to change the mental model that we apply to them. Functional components are essentially interface functions from parameters. They describe everything as it should be at any given time, and help not to think about how to react to changes.
Yes, sometimes you need to learn how to use them correctly , but in the same way we did not learn how to use components in the form of classes right away.
Source: https://habr.com/ru/post/443488/
All Articles