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 (
                <form>
                    {/* Inputs render */}
                </form>
            );
        };
    }

    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 (
                <form>
                    {/* Draft saving meta render */}
                    {/* Inputs render */}
                </form>
            );
        };
    }

    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 (
            <form>
                {/* Draft saving meta render */}
                {/* Inputs render */}
            </form>
        );
    }

    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 (
            <form>
                {/* Draft saving meta render */}
                {/* Inputs render */}
            </form>
        );
    }

    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 (
                <Fragment>
                    <ChatsList changeChat={currentChat => {
                            this.setState({ currentChat });
                        }} />
                    <CurrentChat id={currentChat} />
                    <MessageForm onSubmit={this.handleSubmit} />
                </Fragment>
            );
        };
    }

    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 (
                <Fragment>
                    <ChatsList changeChat={setCurrentChat} />
                    <CurrentChat id={currentChat} />
                    <MessageForm onSubmit={handleSubmit} />
                </Fragment>
            );
        };
    }

    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 or props 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: <p>You clicked 0 times</p>.
      • Also remember to run this effect after you’re done: () => { document.title = 'You clicked 0 times' }.
    • 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' }.

    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?

    Поделиться публикацией

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое