Functional components with React Hooks. Why are they better?

Relatively recently, React.js 16.8 was released, with which hooks became available to us. 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 using classes.


Many perceived the appearance of hooks with criticism, and in this article I would like to talk about some important advantages that functional components with hooks give us, and why we should switch to them.


I will not deliberately delve into the details of using hooks. This is not very important for understanding the examples in this article; a general understanding of React is enough. If you want to read exactly 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, what, and how to use hooks correctly.


Hooks make code reuse easier


Let's imagine a component that renders a simple shape. Something that will simply output a few inputs and allow us to edit them.


Something like this, if greatly simplified, this component would look like a class:


class Form extends React.Component {
    state = {
        // Значения полей
        fields: {},
    };
    render() {
        return (
            
{/* Рендер инпутов формы */}
); }; }

Now imagine that we want to automatically save field values ​​when they change. I suggest omitting declarations of additional functions, like shallowEqualand 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 (
            
{/* Рендер информации о том, когда был сохранен черновик */} {/* Рендер инпутов формы */}
); }; }

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 (
        
{/* Рендер информации о том, когда был сохранен черновик */} {/* Рендер инпутов формы */}
); }

As we see, the difference is not very big yet. We changed the state to a hook useStateand cause saving to draft not in componentDidUpdate, but after rendering the component using the hook useEffect.


The difference that I want to show here (there are others, we will talk about them below): we can get this code out 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 (
        
{/* Рендер информации о том, когда был сохранен черновик */} {/* Рендер инпутов формы */}
); }

Now we can use the hook we useDraftjust wrote in other components! This, of course, is a very simplified example, but reusing the same type of functionality is a very useful feature.


Hooks allow you to write more intuitive code.


Imagine a component (so far in the form of a class), which, for example, displays the current chat window, a list of possible recipients, and a message sending form. Something like this:


class ChatApp extends React.Component {
    state = {
        currentChat: null,
    };
    handleSubmit = (messageData) => {
        makeSomeAPICall(SEND_URL, messageData)
            .then(() => {
                alert(`Сообщение в чат ${this.state.currentChat} отправлено`);
            });
    };
    render() {
        return (
             {
                        this.setState({ currentChat });
                    }} />
                
        );
    };
}

The example is very conditional, but it’s quite suitable for demonstration. Imagine these user actions:


  • Open chat 1
  • Send a message (imagine that the request takes a long time)
  • Open chat 2
  • Receive message about successful sending:
    • "Chat message sent 2"

But the message was sent to chat 1? This happened due to the fact that the class method did not work with the value that was at the time of sending, but with that which was already at the time the request was completed. This would not be a problem in such a simple case, but the correction of such behavior in the first place will 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 (
            
        );
    };
}

Imagine the same user actions:


  • Open chat 1
  • Send a message (the request is taking a long time again)
  • Open chat 2
  • Receive message about successful sending:
    • "Chat message 1 sent"

So what has changed? What has changed is that now for each render for which it is different, currentChatwe are creating a new method. This allows us not to think at all about whether something will change in the future - we are working with what we have now . Each render component closes in itself everything that relates to it .


Hooks save us from the life cycle


This item overlaps with the previous one. React is a library for declaratively describing an interface. Declarative greatly facilitates the writing and support of components, allows you to think less about what would have to be done imperatively if we had not used React.


Despite this, when using classes, we are faced with the component life cycle. If you do not go deeper, it looks like this:


  • Component mount
  • Component update (when changing stateor props)
  • Component Removal

It seems convenient, but I am convinced that it is convenient solely because of habit. 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 the function so that its result uniquely reflects the state of the interface depending on the external parameters and the internal state.


A hook useEffectthat is perceived by many as a direct replacement componentDidMount, componentDidUpdateand so on, is actually intended for another. When using it, we kind of tell the reaction: "After you render this, please perform these effects."


Here is a good example of how the component works with the click counter from a large article about useEffect :


  • React: Tell me what to render with this state.
  • Your component:
    • Here is the result of the rendering:

      Вы кликнули 0 раз

      .
    • And, please, follow this effect, when you're done: () => { document.title = 'Вы кликнули 0 раз' }.
  • React: OK. Updating the interface. Hey, Browser, I am updating the DOM
  • Browser: Great, I drew.
  • React: Super, now I will call the effect that I received from the component.
    • Starts up () => { document.title = 'Вы кликнули 0 раз' }

Much more declarative, isn't it?


Summary


React Hooks allow us to get rid of some problems and facilitate the comprehension and coding of components. You just need to change the mental model that we apply to them. Functional components are essentially interface functions of parameters. They describe everything as it should be at any given time, and help not to think about how to respond 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.


Also popular now: