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!
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.
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”.
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.
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.
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
inMainContainer
and pass the select callback three levels down toSelectableList
, handling all the possible actions inMainContainer
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 useReact.PureComponen
t 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
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:
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:
These downsides are a little boring and cumbersome to manage. That's why Redux was built.
Hope I helped. good luck
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With