ReactJS - Подъемное состояние вверх и сохранение локального состояния

В моей компании мы переносим интерфейсный веб-приложение в ReactJS. Мы работаем с приложением create-response-app (обновлено до версии v16), без Redux. Теперь я застрял на странице, структура которой может быть упрощена следующим изображением:

Структура страницы

Данные, отображаемые тремя компонентами (SearchableList, SelectableList и Map), получаются с тем же запросом на бэкэнд в методе componentDidMount() MainContainer. Результат этого запроса затем сохраняется в состоянии MainContainer и имеет структуру более или менее:

state.allData = {
  left: {
    data: [ ... ]
  },
  right: {
    data: [ ... ],
    pins: [ ... ]
  }
}

LeftContainer получает в качестве prop state.allData.left из MainContainer и передает props.left.data в SearchableList, еще раз как prop.

RightContainer получает в качестве prop state.allData.right из MainContainer и передает props.right.data в SelectableList и props.right.pins на карту.

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

Я решил сохранить в состоянии RightContainer список, содержащий все идентификаторы элементов, отображаемых SelectableList; этот список передается как реквизиты как для SelectableList, так и для Map. Затем я перехожу к SelectableList обратного вызова, что всякий раз, когда делается выбор, обновляется список идентификаторов внутри RightContainer; новые реквизиты поступают как в SelectableList, так и в Map, и поэтому render() вызывается в обоих компонентах.

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

В качестве возможной альтернативы я подумал о добавлении свойства _selected к каждому элементу в state.right.data в MainContainer и передать выбранный обратный вызов на три уровня до SelectableList, обрабатывая все возможные действия в MainContainer. Но как только произойдет событие выбора, это в конечном итоге приведет к загрузке LeftContainer и RightContainer, введя необходимость внедрения логик, таких как shouldComponentUpdate(), чтобы избежать бесполезного render(), особенно в LeftContainer.

Что является/может быть лучшим решением для оптимизации этой страницы с точки зрения архитектуры и производительности?

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

MainContainer.js

class MainContainer extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      allData: {}
    };
  }

  componentDidMount() {
    fetch( ... )
      .then((res) => {
        this.setState({
          allData: res
        });
      });
  }

  render() {
    return (
      <div className="main-container">
        <LeftContainer left={state.allData.left} />
        <RightContainer right={state.allData.right} />
      </div>
    );
  }
}

export default MainContainer;

RightContainer.js

class RightContainer extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      selectedItems: [ ... ]
    };
  }

  onDataSelection(e) {
    const itemId = e.target.id;
    // ... handle itemId and selectedItems ...
  }

  render() {
    return (
      <div className="main-container">
        <SelectableList
          data={props.right.data}
          onDataSelection={e => this.onDataSelection(e)}
          selectedItems={this.state.selectedItems}
        />
        <Map
          pins={props.right.pins}
          selectedItems={this.state.selectedItems}
        />
      </div>
    );
  }
}

export default RightContainer;

Спасибо заранее!

Ответ 1

Как React docs state

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

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

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

Таким образом, вам необходимо поднять эти состояния вверх по дереву, которые также используются в компоненте Siblings. Итак, первая реализация, в которой вы сохраняете selectedItems, как состояние в RightContainer, полностью оправдана и имеет хороший подход, поскольку родительский элемент не нуждается в этом, и этот data разделяется двумя child компоненты RightContainer, и эти два теперь имеют единственный источник истины.

По вашему вопросу:

В качестве возможной альтернативы я подумал о добавлении _изложенного свойства в каждый элемент в state.right.data в MainContainer и передать выбранный обратный вызов на три уровня до SelectableList, обработка всех возможные действия в MainContainer

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

Рассмотрим optimise on performance, вы сами говорите о внедрении shouldComponentUpdate, но вы можете избежать этого, создав свои компоненты, расширив React.PureComponent, который по существу реализует shouldComponentUpdate с shallow сравнением state и props.

Согласно документам:

Если ваша функция React components render() отображает тот же результат учитывая те же реквизит и состояние, вы можете использовать React.PureComponen t для повышение производительности в некоторых случаях.

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

Например, рассмотрим следующий случай

const App = () => {
    <Router>
         <Route path="/" component={Home}/>
         <Route path="/mypage" component={MyComp}/>
    </Router>
}

Теперь, если и Home, и MyComp хотят получить доступ к тем же данным. Вы можете передавать данные в виде реквизита из приложения, называя их через render prop. Однако это легко сделать, подключив оба этих компонента к состоянию Redux с помощью функции connect, например

const mapStateToProps = (state) => {
   return {
      data: state.data
   }
}

export connect(mapStateToProps)(Home);

и аналогично для MyComp. Также его легко настроить действия для обновления соответствующей информации

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

Ответ 2

Мой честный совет по этому поводу. Из опыта:

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

Поскольку Redux инкапсулирует ваше приложение, вы можете думать о хранении таких вещей, как:

  • текущий локатор приложений
  • текущий аутентифицированный пользователь
  • текущий токен откуда-то

Вещи, которые вам понадобятся в глобальном масштабе. react-redux даже позволяет декорировать @connect на компонентах. Так что:

@connect(state => ({ 
   locale: state.locale,
   currentUser: state.currentUser
}))
class App extends React.Component

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

<Navbar {...this.props} />

Все остальные компоненты (или "страницы" ) внутри вашего приложения могут выполнять свое собственное инкапсулированное состояние. Например, страница "Пользователи" может сделать это самостоятельно.

class Users extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      loadingUsers: false,
      users: [],
    };
  }
......

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

Этот подход я сделал это несколько раз, и он работает.

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

Downsides:

  • Вам нужно будет передавать их на компоненты внутреннего уровня.
  • Чтобы обновить состояние от компонентов внутреннего уровня, вам нужно передать функцию, которая обновляет состояние.

Эти недостатки немного скучны и громоздки для управления. Вот почему Redux был построен.

Надеюсь, я помог. удачи

Ответ 3

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