Антипаттерны в React или вредные советы новичкам

Привет, Хабр.

Ровно год прошел с момента, как я начал изучать React. За это время я успел выпустить несколько небольших мобильных приложений, написанных на React Native, и поучаствовать в разработке web-приложения с использованием ReactJS. Подводя итог и оглядываясь назад на все те грабли, на которые я успел наступить, у меня родилась идея выразить свой опыт в виде статьи. Оговорюсь, что до начала изучения реакта, у меня имелось 3 года опыта разработки на c++, python, а также мнение, что во фронтенд разработке нет ничего сложного и разобраться во всем мне не составит труда. Поэтому в первые месяцы я пренебрегал чтением обучающей литературы и в основном просто гуглил готовые примеры кода. Соответственно, примерный разработчик, который первым делом изучает документацию, скорее всего, не найдет для себя здесь ничего нового, но я все-таки считаю, что довольно много людей при изучении новой технологии предпочитают путь от практики к теории. Так что если данная статья убережет кого-то от граблей, то я старался не зря.

Совет 1. Работа с формами


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

Вариант 1. Как делать не надо


В React существует возможность создания ссылки на узел DOM или компонент React.

this.myRef = React.createRef();

C помощью атрибута ref созданную ссылку можно присоединить к нужному компоненту/узлу.

<input id="data" type="text" ref={this.myRef} /> 

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

class BadForm extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
    this.onClickHandler = this.onClickHandler.bind(this);
  }

  onClickHandler() {
    const data = this.myRef.current.value;
    alert(data);
  }

  render() {
    return (
      <>
        <form>
          <label htmlFor="data">Bad form:</label>
          <input id="data" type="text" ref={this.myRef} />
          <input type="button" value="OK" onClick={this.onClickHandler} />
        </form>
      </>
    );
  }
}

Как внутренняя обезьянка может попытаться оправдать данное решение:

  1. Главное, что работает, у тебя еще 100500 задач, а сериалы не смотрены тикеты не закрыты. Оставь так, потом поменяешь
  2. Смотри, как мало кода нужно для обработки формы. Объявил ref и получай доступ к данным откуда хочешь.
  3. Если будешь хранить значение в state, то при каждом изменении вводимых данных все приложение будет рендериться заново, а тебе ведь нужны только итоговые данные. Так этот метод получается еще и по оптимизации хорош, точно оставь так.

Почему обезьянка не права:

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

Вариант 2. Классическое решение


Для каждого поля формы создается переменная в state, в которой будет храниться результат ввода. Атрибуту value присваивается данная переменная. Атрибуту onСhange присваивается функция, в которой через setState() изменяется значение переменной в state. Таким образом, все данные берутся из state, а при изменении данных изменяется state и приложение рендерится заново.

class GoodForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = { data: '' };
    this.onChangeData = this.onChangeData.bind(this);
    this.onClickHandler = this.onClickHandler.bind(this);
  }

  onChangeData(event) {
    this.setState({ data: event.target.value });
  }

  onClickHandler(event) {
    const { data } = this.state;
    alert(data);
  }

  render() {
    const { data } = this.state;
    return (
      <>
        <form>
          <label htmlFor="data">Good form:</label>
          <input id="data" type="text" value={data} onChange={this.onChangeData} />
          <input type="button" value="OK" onClick={this.onClickHandler} />
        </form>
      </>
    );
  }
}

Вариант 3. Продвинутый. Когда форм становится много


У второго варианта существует ряд недостатков: большое количество стандартного кода, для каждого поля необходимо объявить метод onСhange и добавить переменную в state. Когда дело доходит до валидации введенных данных и вывода сообщений об ошибке, то количество кода возрастает еще больше. Для облегчения работы с формами существует прекрасная библиотека Formik, которая берет на себя вопросы, связанные с обслуживанием форм, а также позволяет с легкостью добавить схему валидации.

import React from 'react';
import { Formik } from 'formik';
import * as Yup from 'yup';

const SigninSchema = Yup.object().shape({
  data: Yup.string()
    .min(2, 'Too Short!')
    .max(50, 'Too Long!')
    .required('Data required'),
});

export default () => (
  <div>
    <Formik
      initialValues={{ data: '' }}
      validationSchema={SigninSchema}
      onSubmit={(values) => {
          alert(values.data);
      }}
      render={(props) => (
        <form onSubmit={props.handleSubmit}>
          <label>Formik form:</label>
          <input
            type="text"
            onChange={props.handleChange}
            onBlur={props.handleBlur}
            value={props.values.data}
            name="data"
          />
          {props.errors.data && props.touched.data ? (
            <div>{props.errors.data}</div>
          ) : null}
          <button type="submit">Ok</button>
        </form>
      )}
    />
  </div>
);

Совет 2. Избегайте мутаций


Рассмотрим простое приложение типа to-do list. В конструкторе определим в state переменную, в которой будет храниться список дел. В методе render() выведем форму, через которую будем добавлять дела в список. Теперь рассмотрим, каким образом мы можем изменить state.

Неправильный вариант, приводящий к мутации массива:

this.state.data.push(item);

В данном случае массив действительно изменился, но React об этом ничего не знает, а значит метод render() не будет вызван, и наши изменения не отобразятся. Дело в том, что в JavaScript при создании нового массива или объекта в переменной сохраняется ссылка, а не сам объект. Таким образом, добавив в массив data новый элемент, мы изменяем сам массив, но не ссылку на него, а значит значение data, сохраненное в state, не изменится.

С мутациями в JavaScript можно столкнуться на каждом шагу. Чтобы избежать мутаций данных, для массивов используйте spread оператор либо методы filter() и map(), а для объектов — spread оператор либо метод assign().

const newData = [...data, item];
const copy = Object.assign({}, obj);

Возвращаясь к нашему приложению, стоит сказать, что правильным вариантом изменения state будет использование метода setState(). Не пытайтесь менять состояние напрямую где-либо, кроме конструктора, так как это противоречит идеологии React.

Не делайте так!

this.state.data = [...data, item];

Также избегайте мутации state. Даже если вы используете setState(), мутации могут привести к багам при попытках оптимизации. Например, если вы передадите мутировавший объект через props в дочерний PureComponent, то данный компонент не сможет понять, что полученные props изменились, и не выполнит повторный рендеринг.

Не делайте так!

this.state.data.push(item);
this.setState({ data: this.state.data });

Корректный вариант:

const { data } = this.state;
const newData = [...data, item];
this.setState({ data: newData });

Но даже вариант выше может привести к трудноуловимым багам. Дело в том, что никто не гарантирует, что за время прошедшее между получением переменной data из state и записью ее нового значения в state, само состояние не изменится. Таким образом, вы рискуете потерять часть сделанных изменений. Поэтому в случае, когда вам необходимо обновить значение переменной в state, используя ее предыдущие значение, делайте это следующим образом:

Корректный вариант, если следующее состояние зависит от текущего:

this.setState((state) => {
  return {data: [...state.data, item]};
});

Совет 3. Эмуляция многостраничного приложения


Ваше приложение развивается, и в какой-то момент вы понимаете, что вам нужна многостраничность. Но как же быть, ведь React является single page application? В этот момент вам может прийти в голову следующая безумная идея. Вы решаете, что будете хранить идентификатор текущей страницы в глобальном состоянии своего приложения, например, используя redux store. Для вывода нужной страницы вы будете использовать условный рендеринг, а переходить между страницами, вызывая action с нужным payload, тем самым изменяя значения в store redux.

App.js

import React from 'react';
import { connect } from 'react-redux';
import './App.css';
import Page1 from './Page1';
import Page2 from './Page2';

const mapStateToProps = (state) => ({ page: state.page });

function AppCon(props) {
  if (props.page === 'Page1') {
    return (
      <div className="App">
        <Page1 />
      </div>
    );
  }
  return (
    <div className="App">
      <Page2 />
    </div>
  );
}
const App = connect(mapStateToProps)(AppCon);
export default App;

Page1.js

import React from 'react';
import { connect } from 'react-redux';
import { setPage } from './redux/actions';

function mapDispatchToProps(dispatch) {
  return {
    setPageHandle: (page) => dispatch(setPage(page)),
  };
}

function Page1Con(props) {
    return (
      <>
        <h3> Page 1 </h3>
        <input 
            type="button"
            value="Go to page2"
            onClick={() => props.setPageHandle('Page2')}
        />
      </>
    );
}

const Page1 = connect(null, mapDispatchToProps)(Page1Con);
export default Page1;

Чем это плохо?

  1. Данное решение — пример примитивного велосипеда. Если вы знаете, как сделать такой велосипед грамотно и понимаете на что идете, то не мне вам советовать. В противном случае, ваш код получится неявным, запутанным и излишне сложным.
  2. Вы не сможете пользоваться кнопкой назад в браузере, так как история посещений не будет сохраняться.

Как это решить?

Просто используйте react-router. Это отличный пакет, который с легкостью превратит ваше приложение в многостраничное.

Совет 4. Где расположить запросы к api


В какой-то момент вам понадобилось добавить в ваше приложение запрос к внешнему api. И тут вы задаетесь вопросом: в каком месте вашего приложения необходимо выполнить запрос?
На данный момент при монтировании React компонента, его жизненный цикл выглядит следующим образом:

  • constructor()
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()

Разберем все варианты по порядку.

В методе constructor() документация не рекомендует делать что-либо, кроме:

  • Инициализации внутреннего состояния через присвоение объекта this.state.
  • Привязки обработчиков событий к экземпляру.

Обращения к api в этот список не попадают, так что идем дальше.

Метод getDerivedStateFromProps() согласно документации существует для редких ситуаций, когда состояние зависит от изменений в props. Снова не наш случай.

Наиболее частой ошибкой является расположение кода, выполняющего запросы к api, в методе render(). Это приводит к тому, что как только ваш запрос успешно выполнится, вы, скорее всего, сохраните результат в состоянии компонента, а это приведет к новому вызову метода render(), в котором снова выполнится ваш запрос к api. Таким образом, ваш компонент попадет в бесконечный рендеринг, а это явно не то, что вам нужно.

Так что идеальным местом для обращений к внешнему api является метод componentDidMount().

Заключение


Примеры кода можно найти на github.

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 50

    +5
    А сколько лет статье? Уже даже PureComponent уходит в прошлое, уступая мемоизированным функциональным компонентам на хуках.
      0
      А в чём от них польза? Всё то же самое, и даже больше, можно делать и на классах-компонентах.
        0
        Во всем хуки лучше. Но хуков раньше не было и есть legacy, который нужно поддерживать. Нужно учить что-то новое.
        Из плюсов, например, удобно работать с контекстом и рефами. Проще разбивать на логические блоки. Проще типизировать(TS). Меньше кода, как на этапе разработки, так и после Babel/Terser. Выше скорость исполнения кода(спорный момент), многое зависит от прямоты рук, но если идеальный случай, то функциональные компоненты работают быстрее. И еще куча всего описано в доке.
          +1
          Из плюсов, например, удобно работать с контекстом и рефами. Проще разбивать на логические блоки. Проще типизировать(TS). Меньше кода, как на этапе разработки, так и после Babel/Terser.

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

            0
            Но:
            1) Не запилили улучшения для работы с классовыми компонентами
            2) Со скоростью работы тоже вряд-ли что-то смогут сделать
            3) С типизацией что можно сделать?
            4) И что же такого грандиозного и архитектурно правильного можно сделать на классах, чего нельзя сделать на функциональных компонентах?
              0

              А что не так со скоростью работы классов?

                0
                1) Не запилили улучшения для работы с классовыми компонентами

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


                2) Со скоростью работы тоже вряд-ли что-то смогут сделать

                А можно увидеть какой-то кейз, в котором классовые компоненты тормозят, а прямая замена на функциональные — тормоза убирает?


                3) С типизацией что можно сделать?

                С типизацией как раз у функциональных компонент проблемы. В классовых то что не так?


                4) И что же такого грандиозного и архитектурно правильного можно сделать на классах, чего нельзя сделать на функциональных компонентах?

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


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


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

                  0
                  Спасибо за конструктивную критику.

                  С типизацией как раз у функциональных компонент проблемы. В классовых то что не так?

                  Я не сталкивался именно с проблемами типизации ни FC(функциональных компонентов), ни PC(компонентах на классах), но хуки в FC умеют в выведение типов
                  const [count, setCount] = useState(0);

                  а также тип и значение находяться в непосредственной близости без загромождения кода:
                  const [someObject, setSomeObject] = useState<ISomeInterface>(props.someObject);


                  могли и запилить

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

                  А можно увидеть какой-то кейз, в котором классовые компоненты тормозят, а прямая замена на функциональные — тормоза убирает?

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

                  имеющейся языковой инфраструктурой

                  А что из жизненного цикла PC или FC не относиться(относиться) к языковой инфраструктуре? Не совсем понятен аргумент вообще.

                  в случае классовых компонент аналог хуков был бы значительно более гибок

                  А можно пример желаемого синтаксиса, что-то не приходит мыслей о том как это может выглядеть.

                  функциональный компонент добавляете состояние


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

                  Если вам в компоненте нужно состояние — то значит компонент просто не должен быть функциональным.

                  Почему?
                    +2
                    Я не сталкивался именно с проблемами типизации ни FC(функциональных компонентов), ни PC(компонентах на классах), но хуки в FC умеют в выведение типов

                    Этот тип же в useState прописан. Если вы сделаете что-то типа private someState = new UseState(selector, 0) внутри класса, то он так же выведет


                    а также тип и значение находяться в непосредственной близости без загромождения кода:

                    Это как раз в итоге загромождает код рендер-функции.


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

                    Так смысл в том что это не "хуки хорошие", а "поленились нормально сделать в классах". Ну т.е. немного не то позиционирование. Хотя результат один, конечно.


                    А что из жизненного цикла PC или FC не относиться(относиться) к языковой инфраструктуре? Не совсем понятен аргумент вообще.

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


                    А можно пример желаемого синтаксиса, что-то не приходит мыслей о том как это может выглядеть.

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


                    Почему?

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

                    +1
                    При добавление состояния функциональный компонент перестаёт быть чистой функцией, тогда уж.
                      0
                      Нет. Прочитайте что такое: чистая функция.
                        +3
                        Прочитал. Всё равно нет, так как хук useState убивает детерминированность функции. А хуки useEffect могут добавить побочные эффекты.

                        Теперь предлагаю пойти почитать вам.
                          0
                          Окей, начну с тезиса.
                          "— Каждый раз функция возвращает одинаковый результат, когда она вызывается с тем же набором аргументов

                          — Нет побочных эффектов"
                          habr.com/ru/post/437512

                          В комментариях почти тоже самое, поэтому будем отталкиваться от этого.
                          Подключение dispatch к любому компоненту уже противоречит понятию чистая функция, поэтому я не рассматривал это. И вообще компонент это уже про работу с DOM, поэтому вообще абсурд. Трактую вашу фразу, хуки загрязняют FC.

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

                          Усложняем, добавляем useState(). Вызывая функцию мы получаем… новый экземпляр компонента. Мы можем множество раз его создать, результат один, init state компонента. Затем делаем Synthetic event, например, клик. То есть, мы создадим некое событие, если это событие произвести с любым экземпляром компонента будет один результат, который не повлияет на внешний скоуп, я только вернёт новой состояние, не будем углубляться в VDOM, но это не повлияет ни на что. Например, каррированная функция суммы, тоже является чистой(в идеальном мире).

                          useEffect уже сложнее, ведь это даёт возможность сделать запрос на сервер, что ДЕЙСТВИТЕЛЬНО УБИРАЕТ СТАТУС ЧИСТОЙ, но не обязывает этого делать, и тогда ситуация также самая что и с useState.

                          IMHO: это уже перебор и разговор зашёл в непродуктивное русло.

                          P.s. комментарии больше статьи, спасибо Druu, хорошая дискуссия.
                            +1

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


                            Если же подходить к вопросу более прямолинейно, то:


                            const A = () => <>I am pure</>; // pure
                            
                            const B = () => {
                              const onClick = () => { kill_all_humans(); }
                              return <button onClick={onClick}>kill</button>
                            }; // B itself is pure, but <B/> is "impure" somehow
                            
                            const C = () => {
                              useAnyKindOfHook(); // impure
                              return null;
                            }; // impure

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


                            В случае Б мы снова имеем с чистой функцией, хотя если попытаться воспользоваться продуктом её результата как компонентом, то такой компонент получится impure. Тут уже отдельная песня что считать чистым компонентом, а что нет, т.к. на ФП это натянуть уже не получится, это ведь императивный реакт как IoC вступает в дело и творит что хочет. Стало быть можно придумать какие угодно правила чистоты рас… компонент, либо считать всё грязным всегда вообще (логично, чо).


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


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

                              +2
                              Вызов функционального компонента, сводиться к созданию экземпляра компонента. И сколько раз не вызывайте, будет один результат.

                              Так в то и дело, что с useState это не так! ф-компонент возвращает кусок в-дома. Стейт не является аргументов ф-компонента. С-но, если бы ф-компонент возвращал один и тот же результат, то он бы возвращал один и тот же кусок вдома при разных стейтах (в результате чего смысла от такого стейта бы не было никакого). Но он возвращает разный. Хотя аргументы (пропсы) не меняются.
                              Вот если бы стейт был аргументом — тут другое дело. Но он не аргумент.

                            +1

                            Что вы имели ввиду под "нет"? Любой хук делает вашу функцию impure.

                    +3

                    Да уж, особенно useCallback() появился от большого удобства. Не то, чтобы я не любил или не использовал хуки, просто такое ощущение, что люди стараюся пихать их везде из страха быть не модными. Одни статьи про "Redux на хуках" чего стоят) Нужно ведь понимать, зачем и как работает инструмент, чтобы им пользоваться.

                    0

                    Одно из ключевых преимуществ хуков перед классами это возможность создавать переиспользуемые и комбинируемые куски логики. Хуки являются однозначно более удобным решением чем Render Props, и нередко более удобны, чем HOC'и. Именно возможность удобно создавать кастомные хуки является самой мощной фичей хуков. Если честно, я совсем не против API классов, но там была проблемы именно с выделением кусков логики и привязки к конкретному компоненту. Я не совсем представляю как можно решить именно эту проблему через API классов.

                      0

                      Druu может вам легко возразить на это тем, что концепция классов никак не запрещает группирование единых кусков кода во что-то одно. Это trait-ы, mixin-ы и как их там ещё только не называли. По сути примеси. Было бы желание у создателей React остаться в концепии классов, вернули бы mixin-ы (они когда-то были) и вообще добавили бы много всякого удобного сахара. Но там совсем другой курс партии и работа в рамках ООП им вообще не интересна.


                      Т.е. если сказать более корректно, то это не преимущество хуков перед классами, а преимущество хуков перед конкретной классовой моделью React.

                        0

                        Ну у миксинов были проблемы с конфликтами имен и неявными зависимостями между миксинами, поэтому от них отказались в пользу HOC'ов. Еще из плюсов для хуков это возможность подключить один хук несколько раз. Вообще говоря классы в React тоже имеют мало общего с ООП, а о наследовании компонентов вообще упоминать не принято.
                        Не буду спорить с тем, что принципиально возможно сделать нормальный API и для классов, но в текущей реализации хуки удобнее классов.

                          0

                          Проблемы со сложностью конкретного класса в ООП вполне разрешимы и решаются дальнейшей декомпозицией. Миксины усложняют класс? Значит, нужно вынести их в отдельный!


                          Всего-то нужно было хранить в каждом компоненте список всех его примесей, а при вызове событий — обходить дерево примесей рекурсивно. Это было бы куда удобнее хуков.

                  +1
                  Ну если уж совсем правильно в случае, когда надо обновить стейт на основании пердыдущего, то лучше вот так:
                  this.setState((prevState, props)=>({data: [...prevState.data, item]}))
                    0
                    Благодарю за верное замечание. Дополнил статью.
                    0
                    Интересно, про первый пункт. Видимо автор не знает, что если нужно обновить поле одного объекта в массиве, придется переписывать весь стейт и все перерисовывать. В данном случае, использование setState — антипатерн. Когда я столкнулся с редактированием динамически сгенерированных карточек, которые содержат по полей пять, setState ложил мое приложение…
                      0

                      похоже у вас проблемы с функциональным программированием

                        0

                        Не весь и не всё. "Всё сложно". Когда в игру вступает иммутабельность туда же вступают всякие shallow comparison проверки и разная хитрая ссылочная мемоизация. И получается что некоторый оверхед всё же остаётся, но если приложение построено правильно, то обычно он незначительный.

                        0
                        Дело в том, что никто не гарантирует, что за время прошедшее между получением переменной data из state и записью ее нового значения в state, само состояние не изменится.

                        ЕМНИП, Javascript в браузере — синхронный, и ничего внезапно, во время выполнения участка кода, поменяться на может.

                          +2

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

                            0

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


                            const onClick = () => setTimeout(() => {
                              setState(1);
                              setState(2);
                            }, 0);
                            
                            return (
                              &lt;button onClick={ onClick } /&gt;
                            );

                            (пишу с мобильного, поэтому возможны небольшие ошибки, но общий смысл должен быть понятен)

                              +1
                              В данном коде стейт будет установлен синхронно

                              Нет. Хук setState просто кладет редьюсер в список. Исполняется этот список уже на другом этапе. Т.е. редьюсер setState(2) будет помещен в список до того, как будет применен редьюсер setState(1).


                              А количество рендеров в хуках вообще штука сложная — т.к. исполнение хуков может добавлять хуки :) то у них там просто while-цикл который рендерит компонент до тех пор, пока хуков не останется :)
                              Ну и еще вкорячено ограничение на максимальное количество рендеров после которого цикл обрывается. 20 штук, что ли.

                                0

                                Специально для вас не поленился и создал песочницу. Судя по всему Вы удивитесь, когда заглянете в консоль.

                                  –2

                                  Так setTimeout уберите, и будет у вас один рендер в конце.

                                    +1

                                    В нём то как раз и суть.

                                      0

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

                                        –1

                                        Посмотрите, пожалуйста, в консоль еще раз внимательно.


                                        before update     // запустился обработчик нажатия
                                        rendered          // компонент перерендерился сразу после вызова
                                                          // setState(), не смотря на то, что обработчик
                                                          // еще не закончил работу, т.е. синхронно
                                        after 1st update  // обработчик клика продолжает выполняться
                                        rendered          // упс, render-метод снова был вызван
                                                          // (опять синхронно)
                                        after 2nd update  // а обработчик еще не закончил выполняться
                                          0

                                          Обработчик клика у вас — setTimeout, компонент и не думает перерендериваться после возврата из него. Ну и насколько я понимаю, ваше "Это не всегда так, если обьернуть в setTimeout" — недокументированное поведение, которое дажи патчем может сломаться без всяких предупреждений

                                            0

                                            Да, но он перерендеривается после вызова setState(), который, как видно из логов, вызывается синхронно.

                                            +1
                                            // setState(), не смотря на то, что обработчик
                                            // еще не закончил работу, т.е. синхронно

                                            Это не синхронно. Опуская подробности, setState ставит команду в очередь, с временем, когда ее надо будет выполнить. Это время может быть на "прямо сейчас". В этом случае вам кажется, что код выполняется синхронно.
                                            Наличие синхронности, вообще говоря, показать на примере невозможно — какой бы порядок выполнения ни был, он может быть при асинхронном выполнении. Факт асинхронного исполнения не говорит ничего о том, что выполнение будет обязательно задержано. Лишь о том, что возможно.


                                            В данном конкретном случае, кстати, даже если просто в промис обернуть вместо setTime — то уже выполнение будет задержано.

                                              0
                                              Это не синхронно. Опуская подробности, setState ставит команду в очередь, с временем, когда ее надо будет выполнить.

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


                                              Наличие синхронности, вообще говоря, показать на примере невозможно

                                              Вам следует перечитать доку по event loop в JS, чтобы понять, что в его контексте такое синхронность и асинхронность. Если коротко, из-за его однопоточности между двумя последовательными вызовами console.log() в рамках одной синхронной функции не может вклиниться что-то еще, т.е. как раз пример выше и демонстрирует синхронность. Еще раз призываю Вас обратить внимание на логи — между выводами из callback'а были выведены логи из функции рендера, что намекает на то, что рендер был вызван внутри setState() синхронно, без очереди.
                                              Если очевидные вещи (логи) не доказывают Вам того, что существуют исключительные случаи — предлагаю просто отладить данные вызовы setState(). Для этого просто добавьте debugger в начало функции рендера и прямо перед вызовом setState(). Затем обновите результат в codesandbox и откройте инструменты разработчика. Когда Вы нажмете на кнопку Click ME, процесс выполнения остановится прямо перед вызовом setState() — нажмите два раза Step over (F10 в Chrome) и Вы, к своему удивлению, очутитесь в функции рендера, вместо того, чтобы продолжить выполнение обработчика. Можете проверить call-stack — это не сон, примерно на 14-м уровне вложенности Вы все еще внутри нашего коллбека.
                                              Если и это Вас не убедит, то дальше можно будет попробовать обратиться лично к разработчиками за разъяснениями, но это уже без меня )

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

                                                Я знаю как она работает. В смысле — ее актуальную реализацию, как оно по факту сделано в реакте. А что там "можно реализовать" — дело совершенно несущественное. Конечно, можно, но какое кому дело, если по факту оно работает вполне определенным образом?


                                                Вам следует перечитать доку по event loop в JS, чтобы понять, что в его контексте такое синхронность и асинхронность.

                                                Я прекрасно знаю, не надо объяснять. Так вот — синхронность исполнения доказать нельзя. Потому что асинхронное исполнение может ничем не отличаться по видимому результату от синхронного.


                                                Еще раз призываю Вас обратить внимание на логи — между выводами из callback'а были выведены логи из функции рендера, что намекает на то, что рендер был вызван внутри setState() синхронно, без очереди.

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


                                                Синхронно = обязательно в том же тике (или микротике)
                                                Асинхронно = не обязательно в том же тике (может в следующем, может через 10 тиков, может через 10 секунд

                                                  0
                                                  Синхронно = обязательно в том же тике (или микротике)

                                                  В данном случае рендер исполняется асинхронно, просто в том же тике.

                                                  Вы сами себе противоречите или не упоминаете еще какое-то отличие Вашего понимания "асинхронности". Поясните, пожалуйста, как должна себя вести в таком случае функция рендера (для данного примера), чтобы Вы определили ее выполнение как синхронное?


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


                                                  function onClick() {
                                                    setState();
                                                  }
                                                  
                                                  function onState() {
                                                    someIntermediateFunction();
                                                  }
                                                  
                                                  function someIntermediateFunction() {
                                                    if (...) {
                                                      ...
                                                    } else {
                                                      render();
                                                    }
                                                  }

                                                  Так вот — синхронность исполнения доказать нельзя. Потому что асинхронное исполнение может ничем не отличаться по видимому результату от синхронного.

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


                                                  Давайте представим функцию fn(), которая должна вызывать переданный ей коллбек. Вот Вам утилитарная функция для проверки асинхронного выполнения переданного коллбека:


                                                  function isSyncCheck(fn) {
                                                    let isSync = false;
                                                    fn(() => {
                                                      isSync = true;
                                                    })
                                                  
                                                    return isSync;
                                                  }

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

                                                    0
                                                    Вы сами себе противоречите или не упоминаете еще какое-то отличие Вашего понимания "асинхронности".

                                                    Вы пропустили слово "обязательно". Если оно не может выполниться в тике отличном от текущего — это синхронно. Если может (но при этом пусть и выполняется в текущем в данном случае) — это асинхронно. Конкретно в рассматриваемом случае, да, рендер выполняется в текущем тике. Но может выполняться и в другом — достаточно минимально код изменить.


                                                    Вот Вам утилитарная функция для проверки асинхронного выполнения переданного коллбека:

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


                                                    Для вашей "утилитарной ф-и": const f = (callback) => Math.random() > 0.5? callback(): setTimeout(callback, 0) ;


                                                    f вызывает коллбек асинхронно, но с вероятностью 0.5 ваша ф-я вернет true

                              –1
                              seState() не изменяет состояние компонента мгновенно после вызова, а отправляет запрос на изменение состояния в очередь. Для повышения производительности React может откладывать на некоторое время обновление состояния, а затем выполнить несколько изменений за раз. Поэтому существует вероятность попадания в очередь двух и более запросов на обновление значения одной и той же переменной. Соответственно, после выполнения первого запроса состояние изменится, но второй запрос никак на это не отреагирует и обновит состояние без учета изменений.
                              0

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

                                0

                                Осталось понять как обрабатывать это самое "пользовательское событие".

                                  0

                                  обработчики popstate event и т. п.

                                    0

                                    Это событие не возникнет если в истории не записаны перемещения пользователя между субстраницами.

                                      0

                                      мы же псевдо MPA делаем, значит должны быть записаны

                                        0

                                        Ну так история-то частью стейта не является (в понимании react и redux)

                                          0

                                          Поинт в том, чтобы сделать её таковой. Вернее браузерную историю синхронизировать со стейтом (redux,mobx,react, ...), где источник правды для приложения в стейте, изменяется история через принятые способы изменения, а вся работа с браузерным API локализована в одном месте, подписанном на изменения стейта и генерящим actions для него в случае действий пользователя

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