Избегайте сверления реквизита и дублирования состояния!

Важно найти баланс между сложностью, удобочитаемостью и производительностью в вашем приложении React.

Хотя React по своей природе быстрый, вы, вероятно, столкнетесь с необходимостью оптимизировать производительность повторного рендеринга вашего приложения.
Это может быть особенно актуально, если вы работаете с большими объемами данных ( Подумайте о длинных списках, больших таблицах) или сложных взаимодействиях с пользовательским интерфейсом (Dnd, анимация и т. д.).

Шоры

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

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

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

Государственное управление

Теперь управление состоянием, бесспорно, является одной из самых сложных вещей в React.
В то время как Redux, Mobx и все другие библиотеки управления состоянием пытались, и многие из них действительно преуспели, предоставить разработчикам лучший опыт для многих приложений, сложность также возросла.
Можно определенно утверждать, что это ложится на плечи разработчика, использующего библиотеку не так, как предполагалось изначально, используя их для локального состояния, что React уже сделал хорошо сам по себе.< br /> Я бы сказал, что сложность заключается просто в том, что мы чрезмерно разрабатываем решение для местного управления государством.

Если все, что вы усвоили из этой статьи, это то, что вам не следует использовать Redux или Mobx для локального состояния, это тоже нормально.

React всегда был хорош для управления состоянием

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

Дело в точке

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

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

Форма — это самый внешний компонент.

  • SomeComponent содержит дочерний компонент One.
  • OtherComponent содержит компоненты Two и Three.
  • Один, Два и Три предназначены для отображения 3 разных входных данных и обработчиков обновлений.
function Form({ onConfirm }) {
  const [data, setData] = useState({ one: 1, two: 2, three: 3 });
  return (
    <>
      <SomeComponent one={data.one} onChange={setData} />
      <OtherComponent two={data.two} three={data.three} onChange={setData}/>
    </>
 );
}
function SomeComponent({ one, onChange }) {
  return (
    <div>
      <One one={one} onChange={onChange} />
    </div>
  );
}
function OtherComponent({ two, three, onChange }) {
  return (
    <div>
      <Two two={two} onChange={onChange} />
      <Three two={two} three={three} onChange={onChange} />
    </div>
  );
}
function One({ one, onChange }) {
  return (
    <div onClick={() => onChange(state => ({ …state, one: state.one + 1 }))}>
      {one}
    </div>
  );
};
function Two({ two, onChange }) {
  return (
    <div onClick={() => onChange(state => ({ …state, two: state.two + 2 }))}>
      {two}
    </div>
  );
};
function Three({ three, onChange }) {
  return (
    <div onClick={() => onChange(state => ({ …state, three: state.three + 3 }))}>
      {three}
    </div>
  );
};

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

const DataContext = createContext();
function Form({ onConfirm }) {
  const state = useState({ one: 1, two: 2, three: 3 });
  return (
    <ValueContext.Provider value={state}>
      <SomeComponent />
      <OtherComponent />
    </ValueContext.Provider>
  )
}
function SomeComponent() {
  return (
    <div>
      <One />
    </div>
  );
}
function OtherComponent() {
  return (
    <div>
      <Two />
      <Three />
    </div>
  );
}
function One() {
  const [{ one }, onChange] = useContext(DataContext);
  return (
     <div onClick={() => onChange(state => ({ …state, one: state.one + 1 }))}>
      {one}
    </div>
  );
};
function Two() {
  const [{ two }, onChange] = useContext(DataContext);
  return (
    <div onClick={() => onChange(state => ({ …state, two: state.two + 2 }))}>
      {two}
    </div>
  );
};
function Three() {
  const [{ three }, onChange] = useContext(DataContext);
  return (
    <div onClick={() => onChange(state => ({ …state, three: state.three + 3 }))}>
      {three}
    </div>
  );
};

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

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

const One = memo(({ one, onChange }) => {
  return (
    <div onClick={() => onChange(state => ({ …state, one: state.one + 1 }))}>
      {one}
    </div>
  );
});

Но теперь мы вернулись к исходной точке.
Для оптимальной производительности мы хотим передавать свойства сверху и запоминать наш компонент. Однако удобочитаемость требует от нас использования контекста.
Кто сказал, что мы не можем иметь и то, и другое? Контекст и реквизит не обязательно должны быть противоположными. Давайте использовать их вместе!

Используйте контекст и реквизит в тандеме

Чтобы добиться этого, нам нужно сделать две вещи.

  • Разделите контекст на два.
  • Запомните наши компоненты и передайте реквизиты.

Разделение контекста

Вместо использования одного DataContext мы создадим два отдельных контекста.
А именно, DataContext и OnChangeContext. Названия, конечно, произвольны.

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

const DataContext = createContext();
const OnChangeContext = createContext();
function Form({ onConfirm }) {
  const [data, onChange] = useState({ one: 1, two: 2, three: 3 });
  return (
    <OnChangeContext.Provider value={onChange}>
      <ValueContext.Provider value={data}>
        <SomeComponent />
        <OtherComponent />
      </ValueContext.Provider>
    </OnChangeContext.Provider>
  )
}
function SomeComponent() {
  return (
    <div>
      <One />
    </div>
  );
}
function OtherComponent() {
  return (
    <div>
      <Two />
      <Three />
    </div>
  );
}
function One() {
  const { one } = useContext(DataContext);
  const onChange = useContext(OnChangeContext);
  return (
    <div onClick={() => onChange(state => ({ …state, one: state.one + 1 }))}>
      {one}
    </div>
  );
};
function Two() {
  const { two } = useContext(DataContext);
  const onChange = useContext(OnChangeContext);
  return (
    <div onClick={() => onChange(state => ({ …state, two: state.two + 2 }))}>
      {two}
    </div>
  );
};
function Three() {
  const { three } = useContext(DataContext);
  const onChange = useContext(OnChangeContext);
  return (
    <div onClick={() => onChange(state => ({ …state, three: state.three + 3 }))}>
      {three}
    </div>
  );
};

Как видите, само по себе это не решает нашу проблему, но мы почти у цели!

Проходящий реквизит

В этом примере повторный рендеринг наших подчиненных компонентов SomeComponent и OtherComponent допустим, поэтому мы будем использовать их для использования контекста данных:

const DataContext = createContext();
const OnChangeContext = createContext();
function Form({ onConfirm }) {
  const [data, onChange] = useState({ one: 1, two: 2, three: 3 });
  return (
    <OnChangeContext.Provider value={onChange}>
      <ValueContext.Provider value={data}>
        <SomeComponent />
        <OtherComponent />
      </ValueContext.Provider>
    </OnChangeContext.Provider>
  )
}
function SomeComponent() {
  const { one } = useContext(DataContext);
  return (
    <div>
      <One one={one} />
    </div>
  );
}
function OtherComponent() {
  const { two, three } = useContext(DataContext);
  return (
    <div>
      <Two two={two} />
      <Three three={three} />
   </div>
  );
}
const One = memo(({ one }) => {
  const onChange = useContext(OnChangeContext);
  return (
    <div onClick={() => onChange(state => ({ …state, one: state.one + 1 }))}>
      {one}
    </div>
  );
});
const Two = memo(({ two }) => {
  const onChange = useContext(OnChangeContext);
  return (
    <div onClick={() => onChange(state => ({ …state, two: state.two + 2 }))}>
      {two}
    </div>
  );
});
const Three = memo(({ three }) => {
  const onChange = useContext(OnChangeContext);
  return (
    <div onClick={() => onChange(state => ({ …state, three: state.three + 3 }))}>
      {three}
    </div>
  );
});

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

В заключение

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

Это может выглядеть примерно так:

const ExpensiveComponent = memo(() => {
  return (
    <div>
      <MiddleComponent />
    </div>
  );
});
function MiddleComponent() {
  const { one } = useContext(DataContext);
  return (
    <One one={one} />
  )
}
const One = memo(({ one }) => {
  const onChange = useContext(OnChangeContext);
  return (
    <div onClick={() => onChange(state => ({ …state, one: state.one + 1 }))}>
      {one}
    </div>
  );
});

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

Будь проще!