ReactJS - 提升状态与保持本地状态

ReactJS - Lifting state up vs keeping a local state

在我的公司,我们正在将 Web 应用程序的前端迁移到 ReactJS。 我们正在使用没有 Redux 的 create-react-app(更新到 v16)。 现在我卡在一个页面上,可以通过下图简化结构:

三个组件(SearchableList、SelectableList和Map)显示的数据是在MainContainer的componentDidMount()方法中用相同的后端请求获取的。然后,此请求的结果存储在 MainContainer 的状态中,其结构大致如下:

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

LeftContainer 从 MainContainer 接收作为 prop state.allData.left 并将 props.left.data 传递给 SearchableList,再次作为 prop.

RightContainer 从 MainContainer 接收作为 prop state.allData.right 并将 props.right.data 传递给 SelectableList,将 props.right.pins 传递给 Map。

SelectableList 显示一个复选框以允许对其项目进行操作。每当对 SelectableList 组件的项目进行操作时,它可能会对地图引脚产生副作用。

我决定在 RightContainer 的状态中存储一个列表,该列表保留 SelectableList 显示的所有项目的 ID;此列表作为道具传递给 SelectableList 和 Map。然后我将回调传递给 SelectableList,每当 selection 生成时,都会更新 RightContainer 中的 ID 列表;新道具同时到达 SelectableList 和 Map,因此 render() 在两个组件中都被调用。

它工作正常,有助于将 SelectableList 和 Map 可能发生的所有事情保留在 RightContainer 中,但我想问的是这对于 lifting-state-up单一真实来源 概念。

作为可行的替代方案,我考虑向 MainContainer 中 state.right.data 中的每个项目添加一个 _selected 属性,并将 select 回调向下传递到 SelectableList,处理MainContainer 中所有可能的操作。但是一旦 selection 事件发生,这最终将强制加载 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;

提前致谢!

通过使用 Redux,你可以避免这样的回调并在一个单一的商店中维护整个状态 - 所以让你的 parent 组件连接组件 - 并使左右组件成为哑组件 - 只需传入道具你从 parent 到 child - 在这种情况下你不必担心回调。

如 React 文档所述

Often, several components need to reflect the same changing data. We recommend lifting the shared state up to their closest common ancestor.

There should be a single “source of truth” for any data that changes in a React application. Usually, the state is first added to the component that needs it for rendering. Then, if other components also need it, you can lift it up to their closest common ancestor. Instead of trying to sync the state between different components, you should rely on the top-down data flow.

Lifting state involves writing more “boilerplate” code than two-way binding approaches, but as a benefit, it takes less work to find and isolate bugs. Since any state “lives” in some component and that component alone can change it, the surface area for bugs is greatly reduced. Additionally, you can implement any custom logic to reject or transform user input.

因此,本质上,您需要将那些正在使用 Siblings 组件的状态提升到树中。因此,您首先将 selectedItems 作为状态存储在 RightContainer 中的实现是完全合理的,并且是一种很好的方法,因为父级不需要知道并且这个 data 正在由 RightContainer 的两个 child 组件共享,这两个组件现在拥有单一的真实来源。

根据你的问题:

As feasible alternative I thought of adding a _selected property to each item in state.right.data in MainContainer and pass the select callback three levels down to SelectableList, handling all the possible actions in MainContainer

我不认为这是比第一种方法更好的方法,因为您 MainContainer 不需要知道 selectedItems 或处理任何更新。 MainContainer 没有对这些状态做任何事情,只是将其传递下去。

考虑 optimise on performance,你自己谈论实现 shouldComponentUpdate,但你可以通过扩展 React.PureComponent 创建你的组件来避免这种情况,它实际上实现了 shouldComponentUpdate shallow 比较 stateprops.

根据文档:

If your React component’s render() function renders the same result given the same props and state, you can use React.PureComponent for a performance boost in some cases.

然而,如果多个深度嵌套的组件正在使用相同的数据,那么使用 redux 并将该数据存储在 redux-state 中是有意义的。这样整个App就可以全局访问,并且可以在不直接相关的组件之间共享。

例如考虑以下情况

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

现在如果 Home 和 MyComp 都想访问相同的数据。您可以通过 render prop 调用它们,将数据作为 props 从 App 传递。然而,通过使用像

这样的 connect 函数将这两个组件连接到 Redux 状态可以很容易地完成
const mapStateToProps = (state) => {
   return {
      data: state.data
   }
}

export connect(mapStateToProps)(Home);

MyComp 也是如此。也很容易配置更新相关信息的操作

此外,为您的应用程序配置 Redux 特别容易,您可以将与相同事物相关的数据存储在各个 reducer 中。通过这种方式,您也可以模块化您的应用程序数据

我对此的诚实建议。从经验来看是:

Redux 很简单。它很容易理解和扩展,但您应该在某些特定用例中使用 Redux。

由于 Redux 封装了您的应用程序,因此您可以考虑存储如下内容:

  • 当前应用语言环境
  • 当前经过身份验证的用户
  • 来自某处的当前令牌

您在全球范围内需要的东西。 react-redux 甚至允许在组件上使用 @connect 装饰器。所以喜欢:

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

这些都是作为道具传下来的,连接可以在应用程序的任何地方使用。尽管我建议只使用传播运算符传递全局属性

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

您应用中的所有其他组件(或 "pages")都可以执行自己的封装状态。例如,用户页面可以做它自己的事情。

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

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

您将通过 props 访问 locale 和 currentUser,因为它们是从 Container 组件传递下来的。

这个方法我已经做过很多次了,很管用。

但是,既然你想真正巩固React的知识,那么在做Redux之前你可以把你的状态存储在top-level组件上并传递下去到 children.

缺点:

  • 你将不得不继续将它们传递到内部级别的组件中
  • 要从内层组件更新状态,您必须传递更新状态的函数。

这些缺点有点乏味且难以管理。这就是构建 Redux 的原因。

希望我有所帮助。祝你好运