Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ReactJS - Lifting state up vs keeping a local state

Tags:

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:

Page structure

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!

like image 472
LucioB Avatar asked Oct 05 '17 21:10

LucioB


People also ask

What is the best way to manage state in React?

Which state management is best in React? React's useState is the best option for local state management. If you need a global state solution, the most popular ones are Redux, MobX, and built-in Context API. Your choice will depend on the size of your project, your needs, and your engineers' expertise.

What does lifting state up in React mean?

In React, sharing state is accomplished by moving it up to the closest common ancestor of the components that need it. This is called “lifting state up”.

What is the best method to update state in component React?

To update our state, we use this. setState() and pass in an object. This object will get merged with the current state. When the state has been updated, our component re-renders automatically.

Why we should not directly update state in React?

One should never update the state directly because of the following reasons: If you update it directly, calling the setState() afterward may just replace the update you made. When you directly update the state, it does not change this.


2 Answers

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

like image 119
Shubham Khatri Avatar answered Sep 30 '22 19:09

Shubham Khatri


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

like image 23
João Cunha Avatar answered Sep 30 '22 18:09

João Cunha