Why you should stop using the ācontainer / presentationalā pattern in Redux
TL;DR:
The redux style guide has an important rule: āConnect More Components to Read Data from the Storeā. This rule has some important performance aspects that > are covered with an example in this post.
The āContainer/Presentationalā Pattern used to be the best practice when using redux or any other store management package.
It was explained thoroughly by Dan Abramov and Michael Chan.
If we want to keep it short, a container
usually holds the data fetching logic and data access to store. On the other hand, a presentational
component is more generic and just present the data it gets as props
.
This pattern was common before React Hooks were introduced and probably every developer using React with classes has seen or used it.
But in the world of hooks, this approach can cause serious performance issues (we will cover those later on).
Moreover, the Redux best practices now suggests we should connect more components to read directly from the store (which is quite the opposite to this pattern).
āPrefer having more UI components subscribed to the Redux store and reading data at a more granular level. This typically leads to better UI performance, as fewer components will need to render when a given piece of state changes.ā (The redux docs)
But, whatās the reason behind this rule?
Our example app
Iāve built a small app with a sidebar fetching all the Pokemons (with a Select that decides how many Pokemons to fetch), and a right box showing the selected Pokemon. Clicking on a Pokemon name at the left, will change the image and fetch the relevant data.
The UI looks like this:
In the first approach, I decided to use connect
on a container
, query all the data from the store and also mapDispatchToProps
so Iāll be able to dispatch some data fetching.
I passed the data to my components using props (the example uses styled-components but the actual styles were left behind):
export const PokemonsPage = ({ asyncGetAllPokemons, asyncGetPokemonDetails, setSelectedPokemon, pokemons, selectedPokemonId, selectedPokemon, }) => { return ( <PokemonsPageWrapper> <PokemonsSidebar asyncGetAllPokemons={asyncGetAllPokemons} pokemons={pokemons} setSelectedPokemon={setSelectedPokemon} /> <PokemonView selectedPokemonId={selectedPokemonId} asyncGetPokemonDetails={asyncGetPokemonDetails} selectedPokemon={selectedPokemon} /> </PokemonsPageWrapper> ); }; export default connect( ({ pokemons }) => ({ pokemons: pokemons.data, selectedPokemonId: pokemons.selectedPokemonId, selectedPokemon: pokemons.selectedPokemonData, }), { asyncGetAllPokemons, setSelectedPokemon, asyncGetPokemonDetails, } )(PokemonsPage);
My components just fetch the data and show it based on the props.
I opened the React profiler, recorded a click on a Pokemon name and saw the renders that it triggered.
Hereās the screenshot:
What do we see here?
When clicking on a Pokemon name on the left sidebar, we initiated two renders. In the first one we see the PokemonsSidebar
was rendered and it took 1ms. But, why was it even rendered? It didnāt change, nor did itās props. The reason for that is that the parent component was rendered because of a change in the redux store, so all of itās children should also render.
We can also see that the whole Render duration
was 13.6ms, thatās because inside the PokemonsSidebar
we have 150 li
ās that rendered again for no reason because their parent re-rendered.
In the second approach, I refactored my code to useSelector
and useDispatch
. My presentational components will be connected directly to the store and dispatch the actions.
Hereās my componentās code:
export const PokemonView = () => { const selectedPokemonId = useSelector( ({ pokemons }) => pokemons.selectedPokemonId ); const selectedPokemon = useSelector( ({ pokemons }) => pokemons.selectedPokemonData ); const dispatch = useDispatch(); useEffect(() => { if (selectedPokemonId) { dispatch(asyncGetPokemonDetails(selectedPokemonId)); } }, [selectedPokemonId, dispatch]); return ( <PokemonCard> {selectedPokemon && ( <SmallImage src={`https://pokeres.bastionbot.org/images/pokemon/${selectedPokemon.id}.png`} alt={selectedPokemon.name} /> )} <div>{selectedPokemon?.name}</div> </PokemonCard> ); };
export const PokemonsSidebar = () => { const pokemons = useSelector(({ pokemons }) => pokemons.data); const [pokemonsNumber, setPokemonsNumber] = useState(10); const dispatch = useDispatch(); useEffect(() => { dispatch(asyncGetAllPokemons(pokemonsNumber)); }, [dispatch, pokemonsNumber]); return ( <SidebarWrapper> <PokemonsNumberDropdown setPokemonsNumber={setPokemonsNumber} pokemonsNumber={pokemonsNumber} /> <ul> {pokemons.map((pokemon) => ( <PokemonItem key={pokemon.name} onClick={() => { dispatch(setSelectedPokemon(pokemon)); }} > {pokemon.name} </PokemonItem> ))} </ul> </SidebarWrapper> ); };
Letās see the profiler screenshot:
As we can see here, the PokemonsSidebar
didnāt re-render because it wasnāt subscribed to the selectedPokemon
data from the store, thus all of itās children didnāt re-render. The Render duration
this time is only 0.9ms (compared to 13.6ms)!
These numbers may sound negligible but thatās just an example. My app isnāt complicated and thereās just one component that shouldnāt render (and itās children). What if I had more components? and my components had a heavy render?
Why does it happen?
Since weāre using function components, whenever our parent component renders, the whole tree is marked for render. React then recurses over all children and tries to render them. If our function components arenāt wrapped with React.memo
they will automatically re-render even if their props or state didnāt change. We need to bear in mind that memoization also has some performance impact so wrapping all of our components with React.memo
isnāt always the best approach.
Disclaimer
As you can understand from the last section, this isnāt a Redux only issue, but rather a React behavior. The ācontainer/presentationalā pattern can still be a valid solution as long as the impact is taken into consideration.
Itās important to note that this isnāt just a useSelector
vs connect
issue. Iāve seen examples creating a container with lots of useSelector
s just aiming to mimic this ācontainerā approach and Iāve also seen examples of connect
on many components.
Summary
In this post, weāve covered an important rule that was somewhat hidden inside the whole Redux style guide. If you still havenāt read the style guide, I urge you to do it. It contains many rules that will make your work with Redux straightforward and clear.
I hope everyoneās feeling well and keeping themselves safe!
If you have any questions, Iām available on Twitter.
Feel free to ask or comment, Iād love to hear your feedback!
Thanks,
Matan.
Read More:
The repo containing this example
The Redux best practices
Presentational and Container Components by Dan Abramov
Container Components by Michael Chan