Функциональные компоненты с React Hooks. Чем они лучше?

в 16:15, , рубрики: javascript, React, react-hooks, ReactJS

Относительно недавно вышла версия React.js 16.8, с которой нам стали доступны хуки. Концепция хуков позволяет писать полноценные функциональные компоненты, используя все возможности React, и позволяет делать это во многом более удобно, чем мы это делали с помощью классов.

Многие восприняли появление хуков с критикой, и в этой статье я хотел бы рассказать о некоторых важных преимуществах, которые нам дают функциональные компоненты с хуками, и почему нам стоит перейти на них.

Я намеренно не буду углубляться в детали использования хуков. Это не очень важно для понимания примеров в этой статье, достаточно общего понимания работы React. Если вы хотите почитать именно на эту тему, информация о хуках есть в документации, и если эта тема будет интересна, я напишу статью подробнее о том когда, какие, и как правильно использовать хуки.

Хуки делают переиспользование кода удобнее

Давайте представим компонент, который рендерит простую форму. Что-то, что просто выведет несколько инпутов и позволит нам их редактировать.

Примерно так, если сильно упростить, этот компонент выглядел бы в виде класса:

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

Теперь представим, что мы хотим автоматически сохранять значения полей при их изменении. Предлагаю опустить объявления дополнительных функций, вроде shallowEqual и 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>
        );
    };
}

Тот же пример, но с хуками:

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>
    );
}

Как мы видим, разница пока не очень большая. Мы поменяли стейт на хук useState и вызываем сохранение в черновик не в componentDidUpdate, а после рендера компонента с помощью хука useEffect.

Отличие, которое я хочу здесь показать (есть и другие, о них будет ниже): мы можем вынести этот код и использовать в другом месте:

// Хук 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>
    );
}

Теперь мы можем использовать хук useDraft, который только что написали, в других компонентах! Это, конечно, очень упрощенный пример, но переиспользование однотипного функционала — очень полезная возможность.

Хуки позволяют писать более интуитивно-понятный код

Представьте компонент (пока в виде класса), который, например, выводит окно текущего чата, список возможных получателей и форму отправки сообщения. Что-то такое:

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>
        );
    };
}

Пример очень условный, но для демонстрации вполне подойдет. Представьте такие действия пользователя:

  • Открыть чат 1
  • Отправить сообщение (представим, что запрос идет долго)
  • Открыть чат 2
  • Получить сообщение об успешной отправке:
    • "Сообщение в чат 2 отправлено"

Но ведь сообщение отправлялось в чат 1? Так произошло из-за того, что метод класса работал не с тем значением, которое было в момент отправки, а с тем, которое было уже на момент завершения запроса. Это не было бы проблемой в таком простом случае, но исправление такого поведения во-первых, потребует дополнительной внимательности и дополнительной обработки, и во-вторых, может быть источником багов.

В случае с функциональным компонентом поведение отличается:

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>
        );
    };
}

Представьте те же действия пользователя:

  • Открыть чат 1
  • Отправить сообщение (запрос снова идет долго)
  • Открыть чат 2
  • Получить сообщение об успешной отправке:
    • "Сообщение в чат 1 отправлено"

Итак, что же поменялось? Поменялось то, что теперь для каждого рендера, для котрого отличается currentChat мы создаем новый метод. Это позволяет нам совсем не думать о том, поменяется ли что-то в будущем — мы работаем с тем, что имеем сейчас. Каждый рендер компонента замыкает в себе все, что к нему относится.

Хуки избавляют нас от жизненного цикла

Этот пункт сильно пересекается с предыдущим. React — библиотека для декларативного описания интерфейса. Декларативность сильно облегчает написание и поддержку компонентов, позволяет меньше думать о том, что было бы нужно сделать императивно, если бы мы не использовали React.

Несмотря на это, при использовании классов, мы сталкиваемся с жизненным циклом компонента. Если не углубляться, это выглядит так:

  • Монтирование компонента
  • Обновление компонента (при изменении state или props)
  • Демонтирование компонента

Это кажется удобным, но я убежден в том, что это удобно исключительно из-за привычности. Этот подход не похож на React.

Вместо этого, функциональные компоненты с хуками позволяют нам писать компоненты, думая не о жизненном цикле, а о синхронизации. Мы пишем функцию так, чтобы ее результат однозначно отражал состояние интерфейса в зависимости от внешних параметров и внутреннего состояния.

Хук useEffect, который многими воспринимается как прямая замена componentDidMount, componentDidUpdate и так далее, на самом деле предназначен для другого. При его использовании мы как бы говорим реакту: "После того, как отрендеришь это, выполни, пожалуйста, эти эффекты".

Вот хороший пример работы компонента со счетчиком кликов из большой статьи про useEffect:

  • React: Скажи мне, что отрендерить с таким состоянием.
  • Ваш компонент:
    • Вот результат рендера: <p>Вы кликнули 0 раз</p>.
    • И еще, пожалуйста, выполни этот эффект, когда закончишь: () => { document.title = 'Вы кликнули 0 раз' }.
  • React: Окей. Обновляю интерфейс. Эй, брайзер, я обновляю DOM
  • Браузер: Отлично, я отрисовал.
  • React: Супер, теперь я вызову эффект, который получил от компонента.
    • Запускается () => { document.title = 'Вы кликнули 0 раз' }

Намного более декларативно, не правда ли?

Итоги

React Hooks позволяют нам избавиться от некоторых проблем и облегчить восприятие и написание кода компонентов. Нужно просто поменять ментальную модель, которую мы на них применяем. Функциональные компоненты по сути — функции интерфейса от параметров. Они описывают все так, как оно должно быть в любой момент времени, и помогают не думать о том, как реагировать на изменения.

Да, иногда нужно научиться их использовать правильно, но точно так же и компоненты в виде классов мы научились применять не сразу.

Автор: kshshe

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js