Запрос к API c React Hooks, HOC или Render Prop


    Рассмотрим реализацию запроса данных к API c помощью нового друга React Hooks и старых добрых товарищей Render Prop и HOC (Higher Order Component). Выясним, действительно ли новый друг лучше старых двух.


    Жизнь не стоит на месте, React меняется в лучшую сторону. В феврале 2019 года в React 16.8.0 появились React Hooks. Теперь в функциональных компонентах можно работать с локальным состоянием и выполнять сайд-эффекты. Никто не верил, что это возможно, но все всегда это хотели. Если вы еще не в курсе деталей, за подробностями сюда.


    React Hooks дают возможность наконец-то отказаться от таких паттернов как HOC и Render Prop. Потому что за время использования к ним накопилось ряд претензий:


    RProp HOC
    1. Много компонентов-оберток, в которых сложно разобраться в React DevTools и в коде. (◕︵◕) (◕︵◕)
    2. Сложно типизировать (Flow, TypeScript). (◕︵◕)
    3. Не очевидно, от какого HOC какие props компонент получает, что усложняет процесс дебаггинга и понимание как работает компонент. (◕︵◕)
    4. Render Prop чаще всего не добавляет верстки, хотя используется внутри JSX. (◕︵◕)
    5. Коллизия ключей props. При передаче props от родителей одинаковые ключи могут быть перезаписаны значениями из HOC. (◕︵◕)
    6. Сложно читать git diff, так как смещаются все отступы в JSX при оборачивании JSX в Render Prop. (◕︵◕)
    7. Если несколько HOC, то можно ошибиться с последовательностью композиции. Правильный порядок не всегда очевиден, так как логика спрятана внутри HOC. Например, когда сначала проверяем авторизован ли пользователь, и только потом запрашиваем личные данные. (◕︵◕)

    Чтобы не быть голословной, давайте рассмотрим на примере чем React Hooks лучше (а может все-таки хуже) Render Prop. Будем рассматривать Render Prop, а не HOC, так как в реализации они очень похожи и у HOC больше недостатков. Попробуем написать утилиту, которая обрабатывает запрос данных к API. Я уверена, что многие писали это в своей жизни сотни раз, ну что же посмотрим можно ли еще лучше и проще.


    Для этого будем использовать популярную библиотеку axios. В самом простом сценарии нужно обработать следующие состояния:


    • процесс получения данных (isFetching)
    • данные успешно получены (responseData)
    • ошибка получения данных (error)
    • отмена запроса, если в процессе его выполнения поменялись параметры запроса, и нужно отправить новый
    • отмена запроса, если данного компонента больше нет в DOM

    1. Простой сценарий


    Напишем дефолтный state и функцию (reducer), которая меняет state в зависимости от результата запроса: success / error.


    Что такое Reducer?

    Справочно. Reducer к нам пришел из функционального программирования, а для большинства JS разработчиков из Redux. Это функция, которая принимает предыдущее состояние и действие (action) и возвращает следующее состояние.


    const defaultState = {
      responseData: null,
      isFetching: true,
      error: null
    };
    
    function reducer1(state, action) {
      switch (action.type) {
        case "fetched":
          return {
            ...state,
            isFetching: false,
            responseData: action.payload
          };
        case "error":
          return {
            ...state,
            isFetching: false,
            error: action.payload
          };
        default:
          return state;
      }
    }

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


    Render Prop


    class RenderProp1 extends React.Component {
      state = defaultState;
    
      axiosSource = null;
    
      tryToCancel() {
        if (this.axiosSource) {
          this.axiosSource.cancel();
        }
      }
    
      dispatch(action) {
        this.setState(prevState => reducer(prevState, action));
      }
    
      fetch = () => {
        this.tryToCancel();
        this.axiosSource = axios.CancelToken.source();
        axios
          .get(this.props.url, {
            cancelToken: this.axiosSource.token
          })
          .then(response => {
            this.dispatch({ type: "fetched", payload: response.data });
          })
          .catch(error => {
            this.dispatch({ type: "error", payload: error });
          });
      };
    
      componentDidMount() {
        this.fetch();
      }
    
      componentDidUpdate(prevProps) {
        if (prevProps.url !== this.props.url) {
          this.fetch();
        }
      }
    
      componentWillUnmount() {
        this.tryToCancel();
      }
    
      render() {
        return this.props.children(this.state);
      }
    

    React Hooks


    const useRequest1 = url => {
      const [state, dispatch] = React.useReducer(reducer, defaultState);
    
      React.useEffect(() => {
        const source = axios.CancelToken.source();
        axios
          .get(url, {
            cancelToken: source.token
          })
          .then(response => {
            dispatch({ type: "fetched", payload: response.data });
          })
          .catch(error => {
            dispatch({ type: "error", payload: error });
          });
        return source.cancel;
      }, [url]);
    
      return [state];
    };

    По url из используемого компонента получаем данные — axios.get(). Обрабатываем success и error, меняя state через dispatch(action). Возвращаем state в компонент. И не забываем отменить запрос в случае изменения url или если компонент удалился из DOM. Все просто, но написать можно по-разному. Выделим плюсы и минусы у двух подходов:


    Hooks RProp
    1. Меньше кода. (◑‿◐)
    2. Вызов сайд-эффекта (запрос данных в API) читается проще, так как написан линейно, не размазан по жизненным циклам компонента. (◑‿◐)
    3. Отмена запроса написана сразу после вызова запроса. Все в одном месте. (◑‿◐)
    4. Простой код, описывающий отслеживание параметров для вызова сайд-эффектов. (◑‿◐)
    5. Очевидно, в какой цикл жизни компонента будет выполнен наш код. (◑‿◐)

    React Hooks позволяют писать меньше кода, и это неоспоримый факт. Значит, эффективность вас как разработчика растет. Но придется освоить новую парадигму.


    Когда есть названия циклов жизни компонента все очень понятно. Сначала мы получаем данные после того, как компонент появился на экране (componentDidMount), потом повторно получаем, если поменялся props.url и перед этим руками не забыть отменить предыдущий запрос (componentDidUpdate), если компонент удалился из DOM, то отменяем запрос (componentWillUnmount).


    Но теперь мы вызываем сайд-эффект прям в рендере, нас же учили, что так нельзя. Хотя стоп, не совсем в рендере. А внутри функции useEffect, которая будет выполнять асинхронно что-то после каждого рендера, а точнее коммита и отрисовки нового DOM.


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


    Новая парадигма

    Понимание как работают React Hooks требует осознание новых вещей. Например, разницу между фазами: коммит и рендер. В фазе рендера React вычисляет, какие изменения надо применить в DOM, путем сравнения с результатом предыдущего рендера. А в фазе коммита React применяет данные изменения в DOM. Именно в фазе коммита вызываются методы: componentDidMount и componentDidUpdate. А вот то, что написано в useEffect, будет вызвано после коммита асинхронно и, таким образом, не будет блокировать отрисовку DOM, если вы вдруг случайно решили что-то синхронно много посчитать в сайд-эффекте.


    Вывод — используйте useEffect. Писать меньше и безопаснее.


    И еще одна прекрасная фича: useEffect умеет подчищать за предыдущим эффектом и после удаления компонента из DOM. Спасибо Rx, которые вдохновили команду React на такой подход.


    Использование нашей утилиты с React Hooks тоже намного удобнее.


    const AvatarRenderProp1 = ({ username }) => (
      <RenderProp url={`https://api.github.com/users/${username}`}>
        {state => {
          if (state.isFetching) {
            return "Loading";
          }
    
          if (state.error) {
            return "Error";
          }
    
          return <img src={state.responseData.avatar_url} alt="avatar" />;
        }}
      </RenderProp>
    );

    const AvatarWithHook1 = ({ username }) => {
      const [state] = useRequest(`https://api.github.com/users/${username}`);
    
      if (state.isFetching) {
        return "Loading";
      }
    
      if (state.error) {
        return "Error";
      }
    
      return <img src={state.responseData.avatar_url} alt="avatar" />;
    };

    Вариант с React Hooks опять выглядит более компактным и очевидным.


    Минусы Render Prop:


    1) непонятно добавляется ли верстка или только логика
    2) если надо будет состояние из Render Prop обработать в локальном state или жизненных циклах дочернего компонента придется создать новый компонент


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


    2) Обновлению данных по действию пользователя


    Добавим кнопку, которая отправляет запрос с новым username. Самое простое решение — это хранить username в локальном state компонента и передавать новый username из state, а не props как сейчас. Но тогда нам придется copy-paste везде, где понадобится похожий функционал. Так что вынесем этот функционал в нашу утилиту.


    Использовать будем так:


    const Avatar2 = ({ username }) => {
     ...
         <button
           onClick={() => update("https://api.github.com/users/NewUsername")}
         >
            Update avatar for New Username
         </button>
    
     ...
    };

    Давайте писать реализацию. Ниже написаны только изменения по сравнению с первоначальным вариантом.


    function reducer2(state, action) {
      switch (action.type) {
       ...
       case "update url":
          return {
            ...state,
            isFetching: true,
            url: action.payload,
            defaultUrl: action.payload
          };
        case "update url manually":
          return {
            ...state,
            isFetching: true,
            url: action.payload,
            defaultUrl: state.defaultUrl
          };
       ...
      }
    }

    Render Prop


    class RenderProp2 extends React.Component {
      state = {
        responseData: null,
        url: this.props.url,
        defaultUrl: this.props.url,
        isFetching: true,
        error: null
      };
    
      static getDerivedStateFromProps(props, state) {
        if (state.defaultUrl !== props.url) {
          return reducer(state, { type: "update url", payload: props.url });
        }
        return null;
      }
    
     ...
    
     componentDidUpdate(prevProps, prevState) {
       if (prevState.url !== this.state.url) {
         this.fetch();
       }
     }
    
     ...
    
     update = url => {
       this.dispatch({ type: "update url manually", payload: url });
     };
    
     render() {
       return this.props.children(this.state, this.update);
     }
    }

    React Hooks


    const useRequest2 = url => {
     const [state, dispatch] = React.useReducer(reducer, {
        url,
        defaultUrl: url,
        responseData: null,
        isFetching: true,
        error: null
      });
    
     if (url !== state.defaultUrl) {
        dispatch({ type: "update url", payload: url });
      }
    
     React.useEffect(() => {
       …(fetch data);
     }, [state.url]);
    
     const update = React.useCallback(
       url => {
         dispatch({ type: "update url manually", payload: url });
       },
       [dispatch]
     );
    
     return [state, update];
    };

    Если вы внимательно посмотрели код, то заметили:


    • url стали сохранять внутри нашей утилиты;
    • появился defaultUrl для идентификации, что url обновился через props. Нам нужно следить за изменением props.url, иначе новый запрос не отправится;
    • добавили функцию update, которую возвращаем в компонент для отправки нового запроса по клику на кнопку.

    Обратите внимание с Render Prop нам пришлось воспользоваться getDerivedStateFromProps для обновления локального state в случае изменения props.url. А с React Hooks никаких новых абстракций, можно сразу в рендере вызывать обновление state — ура, товарищи, наконец!


    Единственно усложнение с React Hooks — пришлось мемоизировать функцию update, чтобы она не изменялась между обновлениями компонента. Когда как в Render Prop функция update является методом класса.


    3) Опрос API через одинаковый промежуток времени или Polling


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


    Использование:


    const AvatarRenderProp3 = ({ username }) => (
     <RenderProp url={`https://api.github.com/users/${username}`} pollInterval={1000}>
    ...

    const AvatarWithHook3 = ({ username }) => {
     const [state, update] = useRequest(
       `https://api.github.com/users/${username}`, 1000
     );
    ...

    Реализация:


    function reducer3(state, action) {
     switch (action.type) {
       ...
       case "poll":
         return {
           ...state,
           requestId: state.requestId + 1,
           isFetching: true
         };
       ...
     }
    }

    Render Prop


    class RenderProp3 extends React.Component {
     state = {
      ...
      requestId: 1,
     }
     ...
     timeoutId = null;
     ...
     tryToClearTimeout() {
       if (this.timeoutId) {
         clearTimeout(this.timeoutId);
       }
     }
    
     poll = () => {
       this.tryToClearTimeout();
       this.timeoutId = setTimeout(() => {
         this.dispatch({ type: 'poll' });
       }, this.props.pollInterval);
     };
     ...
    
     componentDidUpdate(prevProps, prevState) {
       ...
    
       if (this.props.pollInterval) {
         if (
           prevState.isFetching !== this.state.isFetching &&
           !this.state.isFetching
         ) {
           this.poll();
         }
    
         if (prevState.requestId !== this.state.requestId) {
           this.fetch();
         }
       }
     }
    
     componentWillUnmount() {
       ...
       this.tryToClearTimeout();
     }
     ...
    

    React Hooks


    const useRequest3 = (url, pollInterval) => {
      const [state, dispatch] = React.useReducer(reducer, {
        ...
        requestId: 1,
      });
    
     React.useEffect(() => {
       …(fetch data)
     }, [state.url, state.requestId]);
    
     React.useEffect(() => {
       if (!pollInterval || state.isFetching) return;
       const timeoutId = setTimeout(() => {
         dispatch({ type: "poll" });
       }, pollInterval);
    
       return () => {
         clearTimeout(timeoutId);
       };
     }, [pollInterval, state.isFetching]);
    
    ...
    }

    Появился новый prop — pollInterval. При завершении предыдущего запроса через setTimeout мы инкрементируем requestId. С хуками у нас появился еще один useEffect, в котором мы вызываем setTimeout. А старый наш useEffect, который отправляет запрос стал следить еще за одной переменной — requestId, которая говорит нам, что setTimeout отработал, и пора уже запрос отправлять за новой аватаркой.


    В Render Prop пришлось написать:


    1. сравнение предыдущего и нового значения requestId и isFetching
    2. очистить timeoutId в двух местах
    3. добавить классу свойство timeoutId

    React Hooks позволяют писать коротко и понятно то, что мы привыкли описывать подробнее и не всегда понятно.


    4) Что дальше?
    Мы можем продолжить расширять функционал нашей утилиты: принимать разную конфигурацию параметров запроса, кеширование данных, преобразование ответа и ошибки, принудительное обновление данных с теми же параметрами — рутинные операции в любом большом веб-приложении. На нашем проекте мы давно это вынесли в отдельный (внимание!) компонент. Да, потому что это был Render Prop. Но с выходом Hooks мы переписали на функцию (useAxiosRequest) и даже нашли некоторые баги в старой реализации. Посмотреть и попробовать можно тут.

    Share post

    Comments 49

      0
      Хоть я сейчас плотно сижу на Vue, но с появлением Hooks начал опять поглядывать в сторону React :)
      0
      Хуки очень круты, но почему все сравнивают с HOC? HOC же аналог декоратора для компонента. Он имеет очень классную особенность — позволяет менять поведение в рантайме и не трогать код основного компонента, тем самым не нарушив работу компонента во всём проекте, а может даже и нескольких. Иногда это жизненно необходимо.

      Почему нельзя совмещать хуки и хоки? Просто нужно без фанатизма. Это не противостояние за власть, а разные инструменты для разных задач. Я как бы понимаю, что и микроскопом можно гвозди забивать, но лучше забивать гвозди молотком, а с микроскопом делать другое.
        +1
        Я как бы понимаю, что и микроскопом можно гвозди забивать, но лучше забивать гвозди молотком, а с микроскопом делать другое.

        Забавно, что вся история с хуками начинается как раз с забивания гвоздей чем угодно кроме молотка — если при проектировании интерфейса не смешивать модель с представлением, то хуки особо и не сдались (потому что представляют собой всего лишь более сильный инструмент для отделения модели и её логики от представленческих вещей типа цикла жизни компонент). Но поскольку модель и логика у фронтэндщиков частенько очень простая и вырожденная — оные фронтэндщики очень любят вписать её сразу в компоненты, а уже потом начать думать, что же с этим делать. И вот тут-то и хуки становятся полезными.
          +1
          Балом правят паттерны (в прочем как и везде).
          Захотел разделения бизнес логики и представления — используешь паттерн Container Component. Захотел изменить что-то в одном месте не меняя поведение в других — HOC. Захотел всё смешать в кучу — написал через хуки.

          Только везде учат, что паттерны должны быть к месту, а не ради использования паттернов, но в React какой-то хайп идёт по каждому новому паттерну (или функционалу, который именуют очередным убийцей). То HOC самый лучший паттерн, который заменяет почти всё, потом появился убийца HOC'а — Render Props, а теперь уже и хуки, которые опять же сравнивают на полном серьёзе с HOC и Render Props как их замену.
          Такое ощущение, что все просто поклоняются тому, о чём пишет Дэн Абрамов. Он написал, что HOC лучший — все это подхватили. Он написал, что HOC ужасен и Render Props теперь лучший — всё форсят Render Props.

          Видимо молодость фронтенда даёт о себе знать. Когда-нибудь большая часть паттернов устаканится и их правильное применение осядет в головах, как это происходит в бэкенде. А сейчас идёт поиск серебряной пули.
            +2
            Захотел разделения бизнес логики и представления — используешь паттерн Container Component.

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

            Во фронтэнде слишком часто встречается проталкивание инструмента везде, где только можно, даже если он не нужен. Есть реакт и его компоненты? А давайте у нас всё-всё будет «компонентом», даже если объективно это вообще ни разу не компонент? А давайте!

            Это вот вообще классическое «когда у вас в руках молоток, все проблемы выглядят незабитыми гвоздями».
            +1

            В клиентском коде логика представления очень часто неотделима от компонента, и кроме как в нем больше нигде не нужна, если мы не говорим о какой-то сложной бизнес логике. Пример — какой-нибудь разворачивающийся блок, выпадающее меню, кнопка со спиннером. Если такую логику начинать выносить — оно вроде как правильно с точки зрения паттернов, но выглядит крайне монструозно. Если слепо всем паттернам следовать — получается Java которую невозможно без крови в глазах читать от словоблудия. JS тем и хорош и элегантен, что можно write less do more. За это надо платить. И это риск. Главное головой думать и баланс соблюдать между "как надо" и "как проще".


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

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

              Смысл не в том, что она нужна где-то еще, смысл в том, чтобы не было мешанины, которую потом трудно поддерживать. В фреймворках с темплейтами отсутствие мешанины форсится самим фреймворком, то есть намешать нельзя by design. В случае реакта намешать можно, по-этому изобретаются способы, как этого не делать — smart/dumb компоненты, рендер пропсы, хуки вот и так далее.

                0

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


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

                  0
                  В Angular в темплейте будь здоров можно логики намешать, впрочем, как и везде.

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


                  HOC, RP и Hooks не просто про разделение, а про возможность выделить некий код как общий и переиспользовать его. Это библиотечные вещи в первую очередь, как подключить что-то внешнее к компоненту.

                  Нет, это в первую очередь про то, как отделить то самое "внешнее" от самого компонента.

                    0
                    набор конструкций, которые можно использовать в темплейте, существенно ограничен

                    Знаете сколько диких конструкций с пайпом и наворотами я видел в ngFor https://angular.io/api/common/NgForOf#local-variables. Я не спорю, что с темплейтом прострелить себе ногу сложнее, но зато и выразительность хуже, в JSX можно очень красивые вещи делать, если с умом подходить.


                    не просто про разделение, а про возможность выделить некий код как общий и переиспользовать его
                    в первую очередь про то, как отделить то самое "внешнее" от самого компонента

                    Вы по-моему то же самое написали, но другими словами. У меня акцент на то, что само по себе выделение должно быть оправдано и должно служить некой цели. Если компонент вещь в себе и логика его никому более не нужна — нет смысла что-то из него выделять в HOC/RP/Hook. Выделять имеет смысл то, что можно переиспользовать.

                      +1
                      Знаете сколько диких конструкций с пайпом и наворотами я видел в ngFor https://angular.io/api/common/NgForOf#local-variables.

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


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

                      Эти "красивые вещи" как раз то, чего не должно быть в логике рендера, так что ничего красивого в них на самом деле нет.
                      Рендер должен иметь только одна ответственность — с-но, рендерить. В случае templatye-based подхода это условие выполнено и в итоге вам достаточно краткого взгляда на темплейт, чтобы понять, что вы получите в результате рендеринга компонента. В случае реакта — у вас plain js, который может делать все что угодно, с-но, эту логику еще распарсить надо (что часто нетривиально), там же обычно происходит и обработка данных. Это не говоря уже о частом использовании антипаттернов вроде HOC/render props, когда в верстке зачем-то оказываются вещи вроде "эта компонента забирает данные оттуда", что вообще в верстке недопустимо и за что следует просто отрывать руки и вон из отрасли.


                      Если компонент вещь в себе и логика его никому более не нужна — нет смысла что-то из него выделять в HOC/RP/Hook.

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


                      Вы же понимаете что вещи вроде best preactices а-ля "не мешайте логику и вид" написаны, что называется, кровью? Все это уже было, давным-давно, и все это было вытеснено более современными и надежными подходами.


                      А ваша логика про "удобство" эта логика из разряда что "работать без шлема удобнее" или "солдату удобнее на посту спать". Ну с этим трудно спорить. Действительно — удобнее. Тем более что через два года с вероятностью 90% типичный фронтендщик сменит место работы и поддерживать то, что он наговнякал, ему не придется. А новый месье скажет "давайте выкинем это говно и напишем новое".

                        0

                        Я несколько более прагматично смотрю. Уровень темплейтов — это самый незначительный уровень в нормальном большом приложении. Все реально важные модули, где нужна архитектура — выше. Если, например, мы говорим о приложениях, которые ворочают данными, особенно в реальном времени, то там ядро данных будет скорее всего вообще ничего не знать о том Vue у вас или допустим Angular или React, потому что там MobX/Redux/RX/plain-vanilla/что-угодно. И по большому счету при правильном подходе должно быть все равно что творится в шаблонах, т.к. верстку переделывают и меняют часто, в том числе полностью с нуля, потому что последние A/B тесты показали, что как было — не годится. И за умную логику в шаблонах/компонентах вместо ядра данных — по голове дадут так и так.


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


                        С точки зрения "по учебнику" я с Вами согласен, но в жизни важны совсем другие вещи. Когда в системе 6000 модулей все должно быть чисто и аккуратно на многих уровнях, и если все правильно сделано на остальных уровнях, шаблон там или не шаблон в глубине уже не имеет значения.

                          +1
                          И по большому счету при правильном подходе должно быть все равно что творится в шаблонах, т.к. верстку переделывают и меняют часто, в том числе полностью с нуля

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

                0
                Описание классов стилей отделено от представления и css классы добавляются через атрибуты.
                Некоторые jquery плагины применяют свою логику в зависимости от добавленных атрибутов. Т.е. там общая логика тоже вынесена из представления.
                Разве это выглядит монструозно?
                Но вместо вынесения логики представления в атрибуты у нас есть всякие миксины, hoc, теперь еще и hooks.
              0
              JustDont меня опередил)
              В общем, хочу сказать следующее. Прихожу к выводу, что нынешние фронтенд фреймворки допустили общую архитектурную ошибку и предлагаются разные решения вокруг этой ошибки.
              Я о разделении ответственности. Компонент занимается и логикой и отображением, что нарушает принцип единственной ответственности.

              Сейчас в классе/функции пишется template/метод render. Там же пишется обработка логики.
              Либо часть логики выносится в миксины, HOC, куда-то еще.
              Но суть от этого не меняется. Вся эта дополнительная логика привязывается к одной сущности — компоненту, который занимается еще и отображением.
              Получается, что связь между отображением и логикой — один к одному.
              В итоге методы, данные, либо props так или иначе смешаны в одном компоненте.
              Так же быть не должно!

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

              И проблема с пересечением данных была бы решена и для компонентов не были бы нужны всякие миксины, HOC, Container Component и прочее. Плюс легко в рантайме можно было бы добавлять/удалять логику.
              При этом аналог HOC с логикой может быть полезен, но не для обертки над компонентом, а для обертки над сущностью с логикой.
                0
                Можете привести пример построения UI и связывания с ним логики на какой-то другой платформе (другом фреймворке/языке), где, по Вашему мнению, такой архитектурной ошибки нет?
                  0
                  Swing, например. Удобством там часто и не пахнет, потому что очень строго MVC везде, и пропустить или упростить бойлерплейт почти нигде нельзя. Зато можно учиться тому, как вообще нормальный MVC должен выглядеть. Главное не тащить его обязаловкой во всё, в том числе и вырожденные случаи.
                    0
                    В чем фундаментальное отличие между
                    JButton button = new JButton("Button");
                    ActionListener actionListener = new MyActionListener();
                    button.addActionListener(actionListener);
                    
                    и react'овским
                    <button onClick={handleClick} />
                    
                    или angular'овским
                    <button (click)="onClick()">Button</button>
                    

                    ?
                      0
                      Вы намеренно приводите кусок кода, в котором нет M, да?
                        +1
                        Я никогда не работал с Swing и привожу первый кусок кода по запросу «Java Swing». Ну хорошо, допустим что кнопка из предыдущего куска когда по клику делает что-то с данными в таблице, а сама таблица связана с моделью:
                        JTable table = new JTable();
                        Model model = new Model();
                        table.setModel(model);
                        
                        Controller controller = new Controller(model);
                        button.addActionListener(controller);
                        
                        а в реакте
                        <Table model={model} />
                        

                        Вопрос остается тем же — в чем фундаментальное отличие от реакта и, особенно, от angular?
                          0
                          Вооот, это уже честнее. А теперь смотрите: реализовать в реакте или еще там каком ангуляре MVC — да никаких проблем (потому что ангуляры и прочие реакты это почти полностью V, остальное их не касается). Но очень часто этого не делают, и потом начинают мужественно бороться с проблемами.

                          Возвращаясь к вашим примерам, в реакте очень редко можно увидеть таблицу, как написано у вас, и очень часто — вот так:
                          <Table data={data} />

                          Думаю, вам понятно, чем это отличается от свинга, в котором так сделать попросту нельзя.
                        0
                        В чем фундаментальное отличие между

                        В количестве словоблудия )

                      0
                      UI в unity3d.
                      Но привести прям пример UI мне сложно, искать надо.
                      Скажу, что UI там строится по тому же принципу, что и все остальное в проекте. Про это можно здесь почитать:
                      docs.unity3d.com/ru/current/Manual/GameObjects.html
                      docs.unity3d.com/ru/current/Manual/UsingComponents.html

                      Чтобы понятней было, можно еще тут про ECS почитать:
                      habr.com/ru/post/358108 (пункт «Что такое ECS»)
                    +1
                    Почему нельзя, можно, только зачем? ) HOC (как и Render Prop) — это способ переиспользования логики/верстки в нескольких местах. Композиция UI представления (компоненты) — всегда была сильной стороной React. Просто выносишь в компонент и переиспользуешь. А вот чтобы шерить логику, не было простого инструмента. Для этого как раз все использовали HOC/Render Prop. Как раз hook — это новый правильный, простой «молоток» для переиспользования логики.
                      0
                      Но HOC и Render Props больше, чем просто способ для повторного использования кода. Во всех статьях их показывают только с одной стороны, как они ужасны по сравнению с хуками в некоторых кейсах. А потом обязательно сделают вывод, что хуки крутые, а хоки и рендер-пропсы уже прошлый век.

                      Покажите мне простой пример паттерна декоратор или адаптер на хуках. Покажите мне аналог scoped-slots на хуках. Окажется ли это проще, чем с HOC или Render Props?

                      Ну и главный вопрос. Какой из этих молотков хуки:
                      виды молотков
                      image
                        0
                        хук — это молоток с крючком, как картинка в статье )) я еще раз повторюсь хуки — это удобный инструмент. Появились хуки — многие вещи стало писать проще. Про scoped-slots c React это можно написать так:
                        const Template = ({ col1, col2 }) => (
                          <div>
                            {col1}
                            {col2}
                          </div>
                        );
                        
                        const TemplateWithData = ({ data }) => (
                          <Template col1={<div>{data.first}</div>} col2={<div>{data.second}</div>} />
                        );
                        
                          0
                          Что-то я вчера разошёлся, видимо накипело из-за других статей по хукам.
                          В вашей говорится о конкретном случае использования. Для этого я бы тоже не стал использовать HOC или Render Props. Но в данном кейсе мне больше симпатизирует паттерн Container Component. Всё-таки если захотим использовать тот же компонент, но с другой логикой получения данных, то в вашем случае придётся создавать ещё один компонент, в который прокинем новый хук. В случае контейнера мы просто создаём новый контейнер, а сам компонент не трогаем. А вообще хуки — это конструкция фреймворка. Их можно совмещать со всеми паттернами, будь то HOC, Render Props, Container Component или любой другой из известных и не очень, которые мы используем для разделения зон ответственности и связанности кода.

                          Но касательно вашего примера — вы написали обычные слоты. Для слотов с ограниченной областью видимости как раз надо передать функцию, т.к. параметры в неё должен передать дочерний компонент. А это уже Render Props.
                          Это может понадобиться при создании универсальных компонентов, например, автокомплита, когда элемент списка в разных местах может иметь разный шаблон. Т.е. мы говорим, что родитель может передать кусок шаблона в дочерний компонент, но в нём можно использовать только определённый набор параметров (к примеру {id, name, title}).
                            +1
                            я так понимаю, вы про такой вариант говорите.
                            const Component1 = ({ getData }) => <div>{getData(1)}</div>;
                            const Component2 = ({ getData }) => <div>{getData(2)}</div>;
                            
                            const WrapperRenderProp = ({ children }) => {
                              const DATA = [1, 2, 3, 4];
                              const getData = i => DATA[i];
                              return <div>{children(getData)}</div>;
                            };
                            
                            const ResultWithRenderProp = () => (
                              <WrapperRenderProp>
                                {getData => (
                                  <div>
                                    <Component1 getData={getData} />
                                    <Component2 getData={getData} />
                                  </div>
                                )}
                              </WrapperRenderProp>
                            );
                            


                            но это можно и так написать без renderProp
                            const Component1 = ({ getData }) => <div>{getData(1)}</div>;
                            const Component2 = ({ getData }) => <div>{getData(2)}</div>;
                            
                            const WrapperWithProps = ({ Template1, Template2 }) => {
                              const DATA = [1, 2, 3, 4];
                              const getData = i => DATA[i];
                              return (
                                <div>
                                  <Template1 getData={getData} />
                                  <Template2 getData={getData} />
                                </div>
                              );
                            };
                            
                            const ResultWithProps = () => (
                              <Wrapper Template1={Component1} Template2={Component2} />
                            );
                            


                            Какой подход выбрать — нужно смотреть на детали. Если, например, есть лэйаут (WrapperWithProps), который в зависимости от темы рендерит разный Template1/Template2 и передает одни и те же пропсы я бы выбрала подход WrapperWithProps.
                            А если надо общий JSX но при этом супер нетривиальная логика с Template внутри я бы выбрала WrapperRenderProp. Но в любом случае это имеет смысл, когда есть общая верстка-раппер (общий JSX).
                            Посыл в статье с хуками про то, что удобно переиспользовать логику. Если нужно переиспользовать верстку, то композиция компонентов всегда была сильной стороной React — можно по-разному.
                    0

                    А кто-нибудь может пояснить, какую все-таки конкретно задачу решают хуки?
                    Ну то есть, если нужен компонент со стейтом — мы просто делаем компонент со стейтом, и хуки не нужны (пример с useRequest на обычном стейте без хуков реализуется точно так же, допустим). Если нужен компонент без стейта — мы делаем компонент без стейта, и хуки тоже не нужны.
                    В каком случае они нужны? Если хотя бы теоретически представить.

                      0

                      Хуки не делают ничего такого, чего нельзя сделать с классами. Это просто другой путь, который команда react-а взяла как за основной. В некотором роде классы теперь legacy.
                      Т.е. нужны они везде где у вас stateful компоненты и вы пишете код, в поддержке и развитии которого вы заинтересованы в будущем.


                      Команда react-a героически победила "проблему с this", заменив её на не менее сложную проблему с оркестрированием замыканий (не очень опытным разработчикам это страшно "выносит мозг", ибо код быстро становится неочевидным). Да и опытным ребусы гадать не всегда хочется. Самое простое это setState + async-useCallback. Всякие фокусы с useRef не для domElement-ов.


                      Я с декабря пишу используя хуки, очень нравится. Но не могу не отметить что это уже какой-то другой react. На мой взгляд писать приложение правильно с хуками сложнее, чем с классами. Требует большей квалификации. Как-то это дисгармонирует с их политикой партии. Хотели упростить, ведь "классы это сложнааа"...

                        0
                        уки не делают ничего такого, чего нельзя сделать с классами. Это просто другой путь, который команда react-а взяла как за основной. В некотором роде классы теперь legacy.

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


                        Команда react-a героически победила "проблему с this"

                        Это какая именно проблема имеется в виду?


                        Т.е. нужны они везде где у вас stateful компоненты и вы пишете код, в поддержке и развитии которого вы заинтересованы в будущем.

                        То есть, фактически, разработчики реакта взяли в заложники свое комьюнити? "Используйте наш кривой костыль вместо полноценного решения, иначе поддерживать не будем"?
                        Фигасе.

                          0
                          То есть, фактически, разработчики реакта взяли в заложники свое комьюнити? «Используйте наш кривой костыль вместо полноценного решения, иначе поддерживать не будем»?

                          Про поддержку классов пока что везде очень осторожные высказывания, что их поддержка никуда не денется и никак не поменяется. Не думаю, что им хочется повторять историю angularJS в исполнении реакта. Никто не побежит переписывать свой код с объектной парадигмы на функциональную просто потому, что разработчикам реакта так нравится больше.
                            0
                            Никто не побежит переписывать свой код с объектной парадигмы на функциональную

                            Хуки же это наоборот объектная парадигма. С-но, к ним основная претензия как раз и состоит в том, что непонятно, зачем нужна наколеночная реализация ООП, когда она уже есть нативная. Другое дело, если бы хуки появились лет 5+ назад, когда классы были еще совсем не в моде. Тогда свои костыльные ООП-подсистемы были еще актуальны.

                              0
                              Не скажу за реакт, но у vue решили отказаться от реализации нативной поддержки классов для компонентов (сейчас у них через декораторы и не совсем красиво) в 3-й версии в пользу своего аналога хуков. Всё больше людей во фронте отдают предпочтение композиции вместо наследования.
                                0
                                Всё больше людей во фронте отдают предпочтение композиции вместо наследования.

                                Так с классами точно такая же композиция. Composition over inheritance — это же вообще ООП-шная придумка. Тут проблема в том, что вместо нативных, полноценных классов, которые поддерживаются на уровне языка, люди зачем-то делают свои, на коленке. Никаких разумных причин, очевидно, для этого быть не может, обычный NIH-синдром.


                                В результате вместо одной нативной реализации ООП, сейчас будет минимум три распространенных — нативная, реакт-хуки и вуе-хуки. И у каждой из них будет свое собственное темное прошлое, туманное будущее и мрачное настоящее.

                            0
                            Это какая именно проблема имеется в виду?

                            "this и ООП в JS это так сложно, что начинающие разработчики и не только путаются и пишут говно-код". Что-то в таком духе. ИМХО, мне было бы легче новичку объяснить что у него там с this сломалось, нежели объяснить почему в его async-callback-е не видно обновлённый state.


                            Фигасе

                            А то. Правда выпиливать классы пока никто вроде не собирается. Но каких-то заметных улучшений в этой области ждать не приходится, они хорошо дали понять, что right react way это хуки и хоки. react-team вообще тяготеет к таким вещам. Хотят везде pure-function, а где не получается, то хотя бы что-то издали похожее на pure-function :) Хотят композицию, иммутабельность, дробление и всё такое.


                            В целом мало-мальски сложное приложение сейчас состоит из такого разнообразного бульона подходов. И render-methods, и контекст, и классовые компоненты (error-boundaries), и слоты, и хоки, и хуки. Может ещё чего забыл. Приправьте к этому какой-нибудь redux (особенно если с proxy, селекторами, викмап-мемоизацией) или mobx и получиться того ещё монстра. Но мне нравится :)

                              0
                              "this и ООП в JS это так сложно, что начинающие разработчики и не только путаются и пишут говно-код"

                              Не понял ничего. Какая проблема с this-то? То, что он ведет себя не так как в других ООП-языках? Так это стрелочными ф-ми решается. Или что-то другое?


                              Хотят везде pure-function, а где не получается, то хотя бы что-то издали похожее на pure-function

                              Ну "казаться а не быть" — это вообще проблема реакт-экосистемы. Везде какие-то нелепые рассуждения про ФП и чистоту, хотя по факту реакторедаксы — чистой воды процедурщина и никакого ФП с чистотой там рядом даже не пробегало.


                              В целом мало-мальски сложное приложение сейчас состоит из такого разнообразного бульона подходов. И render-methods, и контекст, и классовые компоненты (error-boundaries), и слоты, и хоки, и хуки.

                              А потом говорят про простой порог вхождения, ага. Он, может, и был простой — в 2015.
                              И главное-то что это бесполезная сложность — т.к. в данном случае знать надо не как делается Х, Y и Z (что расширяет возможности), а 100 разных способов сделать Х.

                                0

                                Рекомендую прочесть и посмотреть это. Тут они попытались объяснить свою мотивацию. Мои пересказы этого текста вторичны :)

                                  0

                                  Пока писал ответ на меня снизошло озарение. Такое ощущение что экосистема React бодро так шагает к тому как писали PHP сайты 15 лет назад: помесь html, php, js, css & sql в одном файле. Ныне это JSX, JS || TS, cssInJS (и graphQL? :D).

                                    0
                                    Такое ощущение что экосистема React бодро так шагает к тому как писали PHP сайты 15 лет назад

                                    https://en.wikipedia.org/wiki/XHP


                                    С-но, да, "как в былые времена" — это основа реакта :)


                                    Classes confuse both people and machines

                                    Кек. Я не первый раз от команды реакта вижу это "confuse", оно нередко встречается в мотивации по всяким issues.
                                    Но надо ли говорить, что у ребят какое-то свое странное мнение о том, что конфьюзит людей, а что — нет? :)


                                    Conceptually, React components have always been closer to functions.

                                    По-этому, чтобы не писать классы, давайте сделаем, чтобы эта функция от класса ничем не отличалась :)

                            0

                            Хуки — это способ использовать локальный стейт и вызывать сайд-эффекты (например запрос к API) в функциональных компонентах. До этого это было возможно только в компонентах-классах. В доках можно детали посмотре ь https://reactjs.org/docs/hooks-intro.html

                              0
                              Хуки — это способ использовать локальный стейт и вызывать сайд-эффекты (например запрос к API) в функциональных компонентах.

                              Так а зачем кому-то может захотеться использовать функциональный компонент, если в нем есть стейт? В этом и есть вопрос. Нафига это надо? Для чего? Какие проблемы в том, чтобы сделать компонент со стейтом классом?

                                0

                                Потому что это проще писать и меньше строчек кода. Если говорить про сайд-эффекты — useEffect — это ещё и безопаснее. В useEffect переданная функция будет вызван асинхронно в отличие от componentDidMount.

                                  0
                                  Потому что это проще писать и меньше строчек кода. Если говорить про сайд-эффекты — useEffect — это ещё и безопаснее.

                                  Но это ведь просто не правда. Строчек кода столько же, "безопаснее" быть не может в силу полной эквивалентности семантики.


                                  В useEffect переданная функция будет вызван асинхронно в отличие от componentDidMount.

                                  В js любую синхронную ф-ю всегда можно вызвать асинхронно. А вот наоборот — нельзя. Так что синхронный интерфейс в подобных случаях более предпочтителен (тем более, что в 9 случаях из 10 хочется именно синхронной работы componentDidMount).

                                    0
                                    Но это ведь просто не правда. Строчек кода столько же, «безопаснее» быть не может в силу полной эквивалентности семантики.

                                    строчек кода меньше.

                                    В js любую синхронную ф-ю всегда можно вызвать асинхронно. А вот наоборот — нельзя.

                                    в componentDidMount синхронный вызов будет откладывать отрисовку DOM. Иногда это правда надо, но из моего опыта как раз наоборот — 1 случай из 10.

                                    но если нужен синхронный вызов то есть хук — useLayoutEffect
                                    reactjs.org/docs/hooks-reference.html#uselayouteffect
                                      0
                                      строчек кода меньше.

                                      Как меньше, если столько же? Они же 1к1 переводятся. Их не может быть меньше, по определению.


                                      в componentDidMount синхронный вызов будет откладывать отрисовку DOM.

                                      Так нам и не надо практически никогда рисовать дом, пока didmount не отработает. На самом деле, мне даже очень сложно представить случаи, когда нужно обратное поведение. Компонент ведь тогда находится в некорректном стейте, его by design нельзя рендерить в этом случае.


                                      но если нужен синхронный вызов то есть хук — useLayoutEffect

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


                                      В итоге та и остается непонятным, зачем кому-либо хотеть хоть в каком-то кейзе использовать хуки, если минусы — есть, а плюсов — никаких.

                            0

                            deleted

                            Only users with full accounts can post comments. Log in, please.