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

Относительно недавно вышла версия 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 позволяют нам избавиться от некоторых проблем и облегчить восприятие и написание кода компонентов. Нужно просто поменять ментальную модель, которую мы на них применяем. Функциональные компоненты по сути — функции интерфейса от параметров. Они описывают все так, как оно должно быть в любой момент времени, и помогают не думать о том, как реагировать на изменения.


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

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

Похожие публикации

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

    +5
    Хуки позволяют переиспользовать код

    Я конечно понимаю, что в это может быть сложно поверить, но — если вы из класса вытащите всю отчуждаемую логику, то вы точно так же будете и с классами переиспользовать код.

    Классы не заставляют вас прибивать гвоздями вашу логику к ним. Особенно учитывая, что классы компонентов — это голый view, которому и следует быть лишенным всякой логики.
      +2
      А если заменить на «Хуки позволяют удобнее переиспользовать код»?

      Зачастую ведь компоненты это не только view, взять хотя бы пример с сохранением формы.
        0
        А если заменить на «Хуки позволяют удобнее переиспользовать код»?

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

        Зачастую ведь компоненты это не только view, взять хотя бы пример с сохранением формы.

        Скажем так, если усиленно бороться за чистоту MVC, можно и форму представить как чистый view, а всю логику сделать отчуждаемой. Другое дело, что на практике подобным заниматься, если только у нас не какой-то очень особенный проект про формы — это такое себе занятие, и никто в здравом уме не будет выпиливать состояние из формы просто ради соблюдения паттернов.
          0
          Я не вижу никаких существенных отличий между тем, дерните ли вы класс модели и метод, выполняющий некую логику, или же сделаете то же самое, но функцией.

          Основное отличие в this, к которому все привязано в классах. То есть да, отделить можно, но так же ли удобно, как просто вырезать несколько строк кода из компонента?

            +1
            Основное отличие в this, к которому все привязано в классах.

            … который собственно создаёт контекст для операций. Впрочем, статика тоже никуда не делась.

            То есть да, отделить можно, но так же ли удобно, как просто вырезать несколько строк кода из компонента?

            Это уже уровень холивара на тему объектного и функционального программирования: я вам скажу, что у вас получится спагетти-код, где данные сложены в одну кучу, а действия — в другую, и поди разберись, что там и как должно делаться и над какими данными. Вы, если зайдете с позиций функциональщины — тоже найдете много теплых слов в мой адрес.

            Но в общем я это всё начал как раз к тому, чтоб продемонстрировать, что разговоры «хуки лучше классов» почти кругом сводятся к «функциональное программирование лучше ООП». То есть, на самом деле, всё примерно то же самое, и упирается во вкусовщину того, у кого как мысли текут. ООПшно или функционально.
              0
              Основное отличие в this, к которому все привязано в классах. То есть да, отделить можно, но так же ли удобно, как просто вырезать несколько строк кода из компонента?

              А в хуке фактически этот же this на компонент, но только неявный и скрытый в реализации реактовских хуков.

                0
                Хорошо, что-то, определяющее компонент есть, но разве не важно то, что здесь оно скрыто от нас и нам совсем не нужно об этом думать?
                  +1

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

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

                    Так и есть. Сложные ситуации всплывают довольно скоро. Самый такой частый и простой use case это когда в useEffect или в useCallback у нас какой-нибудь async-method есть. На момент его вызова его нутро цепляется к тому замыканию в котором оно было запущено, и соответственно к замороженным иммутабельным state-данным из useState. Как-то их обновить при этом уже невозможно, ибо замыкание есть замыкание. Внутри async-метода может быть несколько асинхронных await, и каждый из них будет в итоге обращаться к потенциально устаревшим данным.


                    Решается такое путём использования ручных useRef. Это аналог classes-properties. Но в целом момент не совсем очевидный и может очень многих поставить во временный тупик.

                      0
                      Просто предположение: а не помогут для такого возвращаемые из эффектов unsubscribe-функции? В них можно было бы сделать что-то вроде отмены.
                        +1

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


                        Что вы туда поместите? Ну допустим какой-нибудь флаг вида isUnmounted: bool. Ок. Как вы его туда поместите? setState? Ну ок. Вы в unsubscribeHandler вы сделали setIsUnmounted(true), и ваш компонент принудительно пере-render-ился. Ваш callbackHandler в useCallback пересоздался. Ок. Теперь у нас есть флаг isUnmounted === true. Помогло ли нам это хоть на грамм? Нет. Ведь внутри уже ранее запущенного async-метода (читайте конечного автомата в замыкании) видны только те значения которые были актуальны на момент его запуска (того самого замыкания на момент запуска). А у нас же иммутабельность и замыкания каждый раз новые. Fail. Приехали :)


                        Собственно почему useRef и ручное ref.current = ... решает проблему? А он мутабелен и сохраняет ссылочную целостность. Он вне контекста замыкания, т.к. useRef всегда возвращает одно и тоже значение. Бинго :) Привет class-properties :-D

                          0

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


                          useEffect(() => {
                              let isCanceled = false;
                              const doThing = async () => {
                                  // ...
                              }
                              doThing()
                                  .then(() => {
                                      if (!isCanceled) {
                                          // Применяем результат 
                                      }
                                  });
                              return () => isCanceled = true;
                          }, [something]);
                            +1

                            Я пробовал так это обыграть. Но обломался. isCanceled будет выставлен в true после первого же setState в doThing. Будет rerender, а это в свою очередь приведёт к unsubscribe-у. А вернуть isCanceled в true будет уже не кому.

                              0

                              А вы пробовали передавать в setState функцию вместо значения? Это позволяет избавиться от замыкания данных.


                              function updater(item) {
                                return items => ([...items, item])
                              }
                              
                              const [data, setData] = useState([]);
                              
                              useEffect(() => {
                                // ...
                                fetchItem()
                                  .then(item => setData(updater(item)));
                                // ...
                              }, [setData, /* ... */]);
                              

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

                                0

                                Вы имеете ввиду, что если для уже демонтированного компонента вызвать не setState(plainValue) а setState(method) то React ругаться на это не будет? Да едва ли. Скорее всего будет всё тоже самое.


                                Ну или я не понял саму идею.

                                  0

                                  Суть не в том, будет ли ругаться React или нет, а в том, что-бы избавиться от замыкания. В этот метод всегда будет приходить актуальное значение стейта.


                                  const [state, setState] = useState(0);
                                  
                                  useEffect(() => {
                                    setState(state + 1);  // setState(0 + 1)
                                  
                                    asyncAction().then(() => setState(state + 1); // setState(0 + 1) state все еще 0
                                  });
                                  
                                  useEffect(() => {
                                    setState(st => st + 1);  // setState(0 + 1)
                                  
                                    asyncAction().then(() => setState(st => st + 1); // setState(1 + 1) state уже обновился
                                  });
                                    0

                                    А, всё я понял Вас. Я слишком обобщённо описал типовую проблему, держа в голове несколько более сложный случай. И в итоге всё смешал.


                                    Касательно вашего предложения использовать callback в setState. На самом деле примерно тоже самое предлагает и Даниил Абрамов делать. Но тут же пишет, что далеко не всегда это возможно.


                                    В общем судите сами, что мы имеем:


                                    • setState(fn) предназначен для смены state. Если вам этот state нужен для чтения ― ...
                                    • setState(fn) работает только в рамках одного state-value. В то время как у нас их может быть несколько. Объединение их в 1 далеко не всегда можно трактовать как разумное решение
                                    • Также предлагается к использованию useReducer, т.к. он оказывается работает очень хитро, в отличие от useState. Дело в том, что reducer можно менять каждый вызов. И если сам callback-reducer поместить в замыкание, то у него всегда будет доступ к самым свежим данным. А отправленные action-ы применяются как раз во время вызова хука (а не до, как можно предположить). Даёшь redux в каждый дом.

                                    Та ситуация которая лично меня настигла была такая… Хм… долго и мутно объяснять. В общем представьте, что вам нужен useIsUnmounted хук. С useRef он реализуется очень просто, ибо мутабельность. А вот без него это уже какие-то безобразия пойдут.


                                    Абрамов рекомендует использовать как раз useRef.current = в своей статье про setInterval.

            +2
            Довольно спорно… эта логика невизуальная? Она должна жить в отдельном сервисе без привязки к компоненту. Асинхронная? Имеем события, подписки или какой другой стандартный путь для оповещений. Причём скорее всего даже вообще не-реакт, а чистый js/ts, переиспользуй-не хочу. RxJs, pub/sub, Redux…

            Лично у меня ощущения от использования хуков — «как бы получить возможности классовых компонентов и „взрослой“* архитектуры и при этом поменьше вникать в эту самую архитектуру». Не знаю, как по мне рано или поздно всё равно придётся разбираться, только наворочено к этому моменту будет больше и рефакторить придётся тоже больше.

            * взрослой = явное управление стейтом и потоками, разделение ответственности, DI, прочий далеко не всем нужный хлам.
              0
              DI назвать хламом без аргументации. Сильно.
                +1
                В следующий раз не забуду поставить тэги «ирония», «сарказм» и «шутка».
            0
            В typescript например, все не так радужно с ООП. Реюз возможен через HoC, а он порождает огромные проблемы с типизацией. Реюз возможен через миксины, но миксины заставляют дублировать интерфейс.
              0
              Да ну здрасьте. Что вам мешает наследовать классы в TS и делать всё без HOC? Там очень обычный ООП.

              Если вы про конкретно реакт-компоненты, то там наследование тоже никто не запрещает, если чо. Просто не рекомендуется, потому что через наследование у вас выйдет тесное связывание компонентов, в то время как через композицию можно сделать и не тесное.
                +1
                Здрасьте.

                Наследование — это пример идеи хорошей в теории, но почему-то на практике редко используемой. Как вы думаете, почему? Потому что джаваскриптеры не умеют в ООП. Потому что наследование на практике не работает.

                Что вам мешает наследовать классы в TS и делать всё без HOC
                Например, то, что в TS нет поддержки мульти-наследования. Не считая миксинов, но см. выше.

                Мне однако безынтересно обсуждать код без кода. Если вы верите в наследование, то попрошу вас предоставить сэмпл «реюзабельного кода» на наследовании. Так, чтобы его аналогом был компонент с четырьма хукам (hooks) или четырьма хоками (HoC). Например: connect, использование двух контекстов и мемоизация коллбека.
                  –1
                  Потому что наследование на практике не работает.

                  Так вот именно. Вы уж сначала там у себя разберитесь, что то ли HOC ужасны, то ли наследование на практике не работает. А то вы почему-то пишете про ужас-ужас, а в следующем посте у вас уже получается, что ужас-ужас — на самом деле наименьший из возможных ужасов.

                  Например, то, что в TS нет поддержки мульти-наследования.

                  Угу, а концептуально мульти-наследование — штука очень интересная и способная породить бесконечно количество граблей для последующего наступания.
                  Странно, чё это люди так не любят мульти-наследование в языки прикручивать?

                  Мне не слишком ясен ваш изначальный посыл. В ООП плохо? А где хорошо-то тогда? Везде плохо. В TS с ООП плохо? А где на фронтэнде с ООП хорошо тогда?

                  Выводы-то какие?
                    +1
                    Я обожаю ситуацию, когда не понимаешь: твой собеседник тролль или вы просто друг друга не поняли?

                    Краткая интерпретация событий
                    Статья: Хуки позволяют переиспользовать код.

                    1-е сообщение (вы): «Я конечно понимаю, что в это может быть сложно поверить, но — если вы из класса вытащите всю отчуждаемую логику, то вы точно так же будете и с классами переиспользовать код.»

                    Ну вот вроде бы я должен был понять это сообщение как «реюз можно и на ООП строить, хуки не нужны».

                    2-е сообщение (я): «В typescript например, все не так радужно с ООП. Реюз возможен через HoC, а он порождает огромные проблемы с типизацией. Реюз возможен через миксины, но миксины заставляют дублировать интерфейс.»

                    Ну вот вроде бы вы должны были понять это сообщение как «в typescript реюз нельзя на ООП строить».

                    3-е сообщение (вы): «Да ну здрасьте. Что вам мешает наследовать классы в TS и делать всё без HOC? Там очень обычный ООП.»

                    Ну вот вроде я должен был понять это сообщение как «да ну шо вы говорите, в typescript на ООП отлично делает реюз»

                    4-е сообщения (я): «Например, то, что в TS нет поддержки мульти-наследования. Не считая миксинов, но см. выше»

                    Ну вот вроде бы вы должны были понять, что «я крайне несогласен с тем, что TS можно делать реюз на ООП, потому что отсутствует мульти-наследование»

                    5-е сообщение (вы): «Угу, а концептуально мульти-наследование — штука очень интересная и способная породить бесконечно количество граблей для последующего наступания.»

                    Я вот должен это как понять? Что если мульти-наследование — штука плохая, то это значит, что в typescript круто делается реюз через ООП?


                    Дополнительная интерпретация
                    Тут допустима еще одна интерпретация вашего текста. Как я понимаю, вы могли иметь в виду, что из компонента A можно вытащить какую-то общую логику вовне через функцию sharedFn. И использовать потом эту функцию sharedFn в другом компоненте B.

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

                    Если вы имели в виду именно это, то имхо, 1) удельный КПД этого действия слишком мал, чтобы его всерьез рассматривать; 2) подобный реюз не «атомарен» и не прячет деталей «абстракции».

                    Например, если реюзаемая логика sharedLogic требует участия в lifecycle методе componentDidMount и componentDidUnmount, то в компоненте A нужно будет вызвать componentDidMount и componentDidUnmount руками. Потом, если в будущем sharedLogic захочет участия в componentDidUpdate, то его нужно будет добавить во все компоненты, которые используют sharedLogic. Без наследования это бессмысленно.

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

                    Как ни крути, все равно бред получается пока что.

                    Выводы
                    Хуки — это хорошо для реюза.

                    В typescript для реюза:

                    • hooks лучше HoC (потому что типы не расходятся и их не нужно дублировать)
                    • HoC лучше mixin (потому что не нужно делать заглушки для кастомных-методов из миксина)
                    • mixin куда лучше наследования (потому что нет мульти-наследования).


                    В javascript для реюза:

                    • hooks чуть лучше HoC (потому что не вводит новых мусорных компонентов, видных в React Developer Tools)
                    • HoC чуть лучше или равноценен mixin (потому что меньше риск получить конфликт на двойном реюзе одинаковой логики; но в теории это можно доработать)
                    • mixin куда лучше наследования (потому что нет мульти-наследования)


                      0
                      Я отвечаю на сообщения, а не на ветки, особенно когда времени прошло уже порядочно, и контекст забылся. Поэтому я, читая ваше «в typescript c ООП плохо» — понял его именно так. Что в TS с ООП плохо. А у вас плохо не в TS, а в реакте с TS, что всё-таки большая разница.

                      Если вы имели в виду именно это, то имхо, 1) удельный КПД этого действия слишком мал, чтобы его всерьез рассматривать; 2) подобный реюз не «атомарен» и не прячет деталей «абстракции».

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

                      И когда вы стейт менеджмент будете делать не на lifecycle-методах, а сторонними средствами — вот тогда вы логику от компонентов сможете спокойно унести (весь необходимый вам стейт менеджмент будет делаться не в компонентах, а отдельно) и не париться всеми этими довольно грустными проблемами модульности голого реакта. Реакт — это не фреймворк, в конце концов, без дополнительных велосипедов для него при написании чего-то сложного так просто не обходится.

                      Будет ли проще сделать свой стейт менеджмент на хуках, чем на HOC? Ну да, скорее всего. А еще проще будет вообще готовый взять, даже если это не к ночи будь помянутый редакс.
                        0
                        И когда вы стейт менеджмент будете делать не на lifecycle-методах, а сторонними средствами
                        Вот тут проблема есть в этом рассуждение.
                        Да, некоторые люди используют реактовский state (без state management).
                        Да, статья написана с примерами кода без state management.
                        Но это не значит, что хуки не решают каких-то проблем.
                        Реакт занимается всем view, а не только рендерингом. Сложную view-логику вы не сможете переложить на state management, потому что там ей не месте.
                        Не говоря уже о том, что react hooks упрощают использование connect, contexts и прочих штук.
                          0
                          Сложную view-логику вы не сможете переложить на state management, потому что там ей не месте.

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

                          Да, некоторые люди используют реактовский state (без state management).
                          Да, статья написана с примерами кода без state management.
                          Но это не значит, что хуки не решают каких-то проблем.

                          Если б они не решали никаких проблем — их бы никто и не написал, не?
                          Я вроде бы обратного и не утверждал. Я лишь комментировал чрезмерные обобщения а-ля «хуки позволяют переиспользовать код», хотя на самом деле там должно быть «хуки позволяют переиспользовать код*», а под * — много мелкого текста о границах применимости и о том, что во многих случаях с этим и без хуков всё нормально.
            +8
            Сначала добавляют в реакт функциональные компоненты, которые вроде как лишены бизнес логики, чистые как слеза младенца и легко тестируемы, и вообще сказка просто.
            А теперь вдруг функция вызывает какие-то неведомые эффекты, читает из чертегознает где хранящегося стейта, создает неопределенное количество замыканий… И все это на каждый вызов рендера.
              0
              Похоже на Вью
                +1
                создает неопределенное количество замыканий

                Создание замыканий не такая уж и проблема, а вот то что многие начнут писать код с утечками памяти из-за closure context sharing'а — это печально.

                +2
                Мне очевиднее componentDidMount и componentWillUnmount, а не useEffect(() => {}). Во тором случае есть нюансы, если не передать список переменных, то он будет вообще работать как componentDidUpdate. Дело привычки конечно, но не очень очевидно.
                  0

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

                  0
                  Почему во всех подобных статьях сравнение хуков с чистым реактом на стейтах, а не с react-redux?, в этом случае и стейты исчезают и всё что пишется — одна строка биндинга компонента и редукса.

                    0
                    Скорее всего, потому что без стейта компоненты на классах не имели смысла, и такие статьи обращены к тем, кто их использует по каким-либо причинам. Ну и не все и не всегда выносится в redux.
                      +1

                      Потому что хуки заменяют в том числе и connect.

                        +1
                        По-этому и хотелось бы сравнить именно эти два подхода.
                          +1
                          Ну вот примеры для redux-react-hook. Без прямых сравнений, увы. Просто, чтобы вы могли понять как это примерно выглядит.

                          Выдергивание стейта:
                          const Component = (props) => {
                             const state = useMappedState(getStateWithMySelector);
                          
                             return (<></>)
                          }
                          


                          Выдергивание стейта по параметризированному селектору:
                          const Component = (props) => {
                             const todo = useMappedState(useCallback(
                               state => state.todos.get(props.id),
                               [props.id],
                             ));
                          
                             return (<></>)
                          }
                          


                          Диспатчинг:
                          const Component = (props) => {
                            const dispatch = useDispatch();
                          
                            const handleClose = useCallback((event) => {
                              dispatch(tryHideSnackBar({ snackId: props.id }));
                            }, [props.id]);
                          
                             return (<></>)
                          }
                            0
                            Это же на каждый рендер будет создваться новая функция в замыкании(
                              0

                              Не совсем так. На каждый, где отличается props.id, а это редкие ситуации.

                                +1

                                Что вы, что вы. Абсолютно на каждый. Никакие props.id тут ничего не поменяют. Другое дело, что useCallback (а лучше useAutoCallback) вернёт ранее созданный метод, а не новосозданный, если props.id не был изменён.


                                Все эти useEffect, useLayoutEffect, useCallback и что там ещё есть, таки действительно порождают новые методы в замыкании раз за разом. Другой вопрос, что это обычно не является критичным с точки зрения производительности.

                                  0
                                  Если говорить строго, то да, на каждый, конечно. Просто дальше она никуда не попадет и ни на что не повлияет.
                                +2
                                Да, действительно, будет каждый рендер создаваться новая функция в замыкании — это лишняя нагрузка на GC, что совсем не радует.
                                НО! То, что функция создается — еще не значит, что именно она будет использоваться. Здесь useCallback используется для мемоизации. И поэтому дочерние компоненты не будут зазря перерендериваться.
                        0
                        Зачем у вас в примерах кода после
                        extends React.Component
                        идет знак "="?
                          0

                          Писал все в текстовом редакторе без обработки синтаксиса, на автомате поставил =. Спасибо за замечание, поправлю.

                            0
                            Код ревью :)
                            А по делу — спасибо, что делитесь с русскоязычным сообществом.
                          +1
                          Хуки позволяют писать более интуитивно-понятный код
                          — ну конечно, всякий новый функционал, заметно отличный от того, к чему мы привыкли за годы, всегда объявляется «интуитивно-понятным»

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

                          — что???!?! Не похож на React? Базовый функционал самого Реакта?

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

                            Так лишь бы не классы, ведь классы — это злое зло :) (Неважно, что в 2015 они презентовались как «Наконец-то в JS появятся классы как во взрослых языках, теперь можно будет делать ООП!»)

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


                            Более точно — у нас появился React.js 2.0 (если кто ещё НЕ заметил сам).

                            Произошло тоже что с Angular — когда появился Angular 2.0

                            Всё что вы знали и писали до этого (до React.js 16.8) можно забыть и выкинуть в мусорку.

                            Никто в здравом уме НЕ будет мешать хуки и классы в одном коде.

                            А нам НЕ обещают, что классы будут вообще развивать в будущем.
                            Точнее, — обещают НЕ развивать классы в React.js вообще.
                            Нет, их не выкинут — но смысл использовать "классовый подход" в новых проектах — исчезает. До нуля.

                            Вывод — выкинуть все учебники и патерны и учить по новой хуки.

                            Это будущее React.js которое уже наступило, хотите вы этого или нет.

                              0
                              Никто в здравом уме НЕ будет мешать хуки и классы в одном коде.

                              Хохмы ради: with-react-hooks


                              Всё что вы знали и писали до этого (до React.js 16.8) можно забыть и выкинуть в мусорку

                              Официальная позиция react-команды — оставьте код в покое, будет надо — перепишете, нет — не перепишете. Дескать пуская старый код остаётся на классах.

                                –1
                                Официальная позиция react-команды — оставьте код в покое, будет надо — перепишете, нет — не перепишете. Дескать пуская старый код остаётся на классах.
                                Это они так пошутили, поди. «Реактивщикии шутят, мистер Дэн?» (С)

                                Переписать потребуется всё! (С)

                                Вообще-то им надо было переименовать React.js в Hook.js — но они этого не сделали (как и ангулярщики). — Бренд, то да сё. Поди.

                                Вот если сейчас начать проект, то нужна библиотека React UI и React роутер — а на хукак их просто нет! (React роутер версии V будет(!) уже на хукак).

                                Библиотек React UI на хукак просто нет ещё. И все живые из них надо будет переписать полностью, добавив в название библиотеки слово hook. — Иначе почти(!) никак.

                                Переписать потребуется всё! (С)

                                Ну, само собой, народ уже это осознаёт и спрашивает — как обычно в революционный период — ТРИ вопроса:

                                1. Почему надо было кидать старые добрые классы и идти на… хуки? (Дэн собирается написать статью об этом, — о том, откуда вообще «ноги растут» — то есть — откуда пошла такая «ересь функциональная», кто это породил и «кого гасить за это».
                                2. Как находясь в старом добром и понятном классовом обществе использовать хуки?
                                3. Как бежать впереди паровоза, используя хуки, но тащить за собой отставшие классовые массы?


                                Дэн, как Лев Толстой, продолжает писать длинные статьи-романы, которые все заносят в закладки, но никто не читает, ибо длинные статьи никто не читает сейчас вовсе.

                                Дэн негодует, что его просят не писать такие длинные статьи и Дэн собирается начать издавать их платной… книгой (или по подписке типа), так как заплатившие никогда не ругают автора за ТОЛСТЫЙ роман — они просто ставят книгу на полку, но не ругают за то, что за свои 25$ получили 500 страниц текста, а не 50 страниц текста.

                                Переписать потребуется всё! (С)
                                  0
                                  Переписать потребуется всё! (С)

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


                                  Ну вот давайте возьмём эту цитату:


                                  Библиотек React UI на хукак просто нет ещё. И все живые из них надо будет переписать полностью, добавив в название библиотеки слово hook. — Иначе почти(!) никак.

                                  Пусть это будет, скажем, antd. И зачем его переписывать на хуки? Что мешает писать на хуках только новый код?


                                  С точки зрения потребителя этих библиотек вам глубоко плевать хуки там или классы. Интерфейс взаимодействия с ними один и тот же. С точки зрения мейнтейнера — код на классах, вероятно, будет работать ещё пару мажорных версий. Или вообще всегда. Там новых вкусных фич не будет. Сам то он не поломается.


                                  Моя команда не планирует ничего на хуки переписывать. А новый код мы пишем на них с альфы.


                                  React роутер версии V будет(!) уже на хукак

                                  Да хоть в версии X. Пока я не лезу в его код мне нет никакого дела, классы там или хуки. Да и вообще у меня на него аллергия, на этот ваш распиаренный react router.


                                  Дэн, как Лев Толстой, продолжает писать длинные статьи-романы, которые все заносят в закладки, но никто не читает, ибо длинные статьи никто не читает сейчас вовсе.

                                  При всей моей нелюбви к Данилу, я таки прочитал все эти статьи. И нашёл это полезным :)

                              0
                              и если эта тема будет интересна, я напишу статью подробнее о том когда, какие, и как правильно использовать хуки.


                              Думаю, многие будут благодарны :)

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

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