-->

ReactJS - Lifting state up vs keeping a local stat

2019-01-02 21:15发布

问题:

At my company we're migrating the front-end of a web application to ReactJS. We are working with create-react-app (updated to v16), without Redux. Now I'm stuck on a page which structure can be simplified by the following image:

The data displayed by the three components (SearchableList, SelectableList and Map) is retrieved with the same backend request in the componentDidMount() method of MainContainer. The result of this request is then stored in the state of MainContainer and has a structure more or less like this:

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

LeftContainer receives as prop state.allData.left from MainContainer and passes props.left.data to SearchableList, once again as prop.

RightContainer receives as prop state.allData.right from MainContainer and passes props.right.data to SelectableList and props.right.pins to Map.

SelectableList displays a checkbox to allow actions on its items. Whenever an action occur on an item of SelectableList component it may have side effects on Map pins.

I've decided to store in the state of RightContainer a list that keeps all the ids of items displayed by SelectableList; this list is passed as props to both SelectableList and Map. Then I pass to SelectableList a callback, that whenever a selection is made updates the list of ids inside RightContainer; new props arrive in both SelectableList and Map, and so render() is called in both components.

It works fine and helps to keep everything that may happen to SelectableList and Map inside RightContainer, but I'm asking if this is correct for the lifting-state-up and single-source-of-truth concepts.

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. But as soon as a selection event occurs this will eventually force the loading of LeftContainer and RightContainer, introducing the need of implementing logics like shouldComponentUpdate() to avoid useless render() especially in LeftContainer.

Which is / could be the best solution to optimise this page from an architectural and performance point of view?

Below you have an extract of my components to help you understand the situation.

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;

Thanks in advance!

回答1:

As React docs state

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.

So essentially you need to lift those state up the tree that are being used up the Siblings component as well. So you first implementation where you store the selectedItems as a state in the RightContainer is completely justified and a good approach, since the parent doesn't need to know about and this data is being shared by the two child components of RightContainer and those two now have a single source of truth.

As per your question:

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

I wouldn't agree that this is a better approach than the first one, since you MainContainer doesn't need to know the selectedItems or handler any of the updates. MainContainer isn't doing anything about those states and is just passing it down.

Consider to optimise on performance, you yourself talk about implementing a shouldComponentUpdate, but you can avoid that by creating your components by extending React.PureComponent which essentially implements the shouldComponentUpdate with a shallow comparison of state and props.

According to the docs:

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.

However if multiple deeply nested components are making use of the same data, it makes sense to make use of redux and store that data in the redux-state. In this way it is globally accessible to the entire App and can be shared between components that are not directly related.

For example consider the following case

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

Now here if both Home and MyComp want to access the same data. You could pass the data as props from App by calling them through render prop. However it would easily be done by connecting both of these components to Redux state using a connect function like

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

export connect(mapStateToProps)(Home);

and similarly for MyComp. Also its easy to configure actions for updating relevant informations

Also its particularly easy to configure Redux for your application and you would be able to store data related to the same things in the individual reducers. In this way you would be able to modularise your application data as well



回答2:

My honest advice on this. From experience is:

Redux is simple. It's easy to understand and scale BUT you should use Redux for some specific use cases.

Since Redux encapsulates your App you can think of storing stuff like:

  • current app locale
  • current authenticated user
  • current token from somewhere

Stuff that you would need on a global scale. react-redux even allows for a @connect decorator on components. So like:

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

Those are all passed down as props and connect can be used anywhere on the App. Although I recommend just passing down the global props with the spread operator

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

All other components (or "pages") inside your app can do their own encapsulated state. For example the Users page can do it's own thing.

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

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

You would access locale and currentUser through props because they were passed down from the Container components.

This approach I've done it multiple times and it works.

But, since you wanted to really consolidate the knowledge of React first, before doing Redux you can just store your state on the top-level component and pass it down to the children.

Downsides:

  • You're gonna have to keep passing them down into inner level components
  • To update state from the inner level components you're gonna have to pass the function that updates the state.

These downsides are a little boring and cumbersome to manage. That's why Redux was built.

Hope I helped. good luck



回答3:

By using Redux you can avoid such callbacks and maintain the whole state in one single store - so make your parent component connected component - and make left and right components dumb ones - and just pass in the props you get from parent to child - and you don't have to worry about callbacks in this case.



标签: