Background and Component state
Sometimes the UI code that a Component generates needs to change after the initial render (e.g. in response to a user interaction or network request). To enable these dynamic UI’s, React gives us Component state. Component state is an extremely useful and well-designed feature, but when used incorrectly, allows us to create brittle Components that are a nightmare to maintain, reuse, and test.
The problem
The problem arises when we attempt to use Component state to manage the state of an entire application. In real-world apps, certain types of state need to be shared between Components. A common example of this is authentication state, because many different Components of an app need to know if a user is logged in and who that user is. Let’s consider two components in an imaginary application: <UserMenu />
, a dropdown user menu that lives somewhere in the nav bar, and <LikeButton />
, a button somewhere in the body of the page that allows a user to like a post. Both of these components need to know who is logged in. It is possible to share the user data between these two components by keeping it in a parent's component state, e.g. <App />
, and passing it down through the component hierarchy as props. Unless an application is extremely small or mostly stateless, this becomes a headache very quickly.
When we take the shared component state approach, we end up creating large quantities of "pass-through" props, which do little else other than shuttle along data and clutter up all the components in between. On top of that, the end users of these pass-through props (UserMenu and LikeButton) end up with a large prop surface area. In order for them to render, their parent must supply them with the many props that they need.
Components that require many props are more difficult to reuse. (<LikeButton />
vs <LikeButton loggedIn={this.props.loggedIn} username={this.props.username} likePost={this.props.likePost} />
). All those props have to be typed out every time we want to use a LikeButton
. This problem becomes even worse when testing a component, as each function and piece of data passed into a Component as a prop may need to be mocked when being tested.
Things get even more messy when a Component needs to change the shared component state. Imagine this: <App />
passes an onLike
callback through the component hierarchy down to <LikeButton />
. When a user clicks the button, <LikeButton />
makes a network request and then calls onLike
. The callback calls setState
on <App />
to record the change to the shared state. This type of arrangement creates complicated relationships between components that are very difficult to understand and change.
How Redux helps
With Redux, all of our data lives in a single Store, and Components can subscribe to only the data they need, from wherever they are being mounted. The shared state previously stored in <App />
in our example is moved to the Redux store. When <LikeButton />
mounts, it simply subscribes to the data that it needs from the Store - the programmer doesn’t have to type out a bunch of props being passed in from it’s parent. And if <LikeButton />
needs to change shared application state, it can import actions or action creators directly and dispatch them. Instead of a method on <App />
, onLike
becomes an action creator.
Conclusion
Redux helps us do less typing, write less complicated code, and develop Components that are free of coupling and easy to reuse.
Top comments (10)
I fully agree with the problem analysis in your post. I am using React/Redux in several projects, and it's most likely the cleanest UI code I have ever written.
However, react-redux has its own set of problems and hick-ups. Bear in mind that, even though you may save some typing when calling a "connected" component, the data dependency still exists; it's just hidden, it did not disappear. What we just introduced is a dependency of a component to something other than its props and its own state. It's a dependency to the store. That actually conflicts with the original design idea of React ("render as a pure function") because you add more data "through the back door". You may argue "but it still works!". Fair point. But did you ever try to implement
shouldComponentUpdate
with something other thanreturn true;
? Think about it. You want to bail out of updates as high up in the component tree as possible to save on computation/rendering time. However, you don't actually know the dependencies of your children (or grand children or...) because they might get their data from the redux store. The sad fact is: you can't know that, because component nesting is recursive. You can't even hard code it because you don't know what your children will be tomorrow or in two months. React-Redux breaks one of the core assumptions of React. Beware of that when you code.Also, many of the pre-made React components you can pick up on the NPM repository are not ready for a React-Redux treatment; they do not implement the "controlled component" idea. Either they do not offer the required callbacks, or do not implement them properly (i.e. they still hold internal state). I usually end up rewriting almost every component myself. I had a look at quite a few React component libraries; and none of them seemed suitable for React-Redux at that time.
Thanks for the thoughtful reply, Martin.
I would argue that connected components still "render as a pure function". The
connect
HOC ofreact-redux
creates a container component behind the scenes which wraps the original component and passes data to it as props. The component that we wrote is not modified, only wrapped. It still depends strictly on its props and state. A connected component can easily be used as an unconnected component with no changes to the code.You make a good point about
shouldComponentUpdate
, although this is not a problem I have encountered frequently. My personal approach to avoid problems like this is to keep the component hierarchy as flat as possible and have components depend on data exclusively from the store. This way each component can decide for itself if it should update. I'm curious in what situations you have encountered the shouldComponentUpdate problem?I certainly agree with you about pre-made components, I almost always implement my own, but I'm not sure I'm comfortable blaming that on Redux. When I have tried to use pre-made UI components in any system, not just React apps, they have usually ended up being more trouble than they are worth.
Thanks again for your reply, you've given me lots to think about :)
Hi!
About the
connect
function: I fully agree that no harm is done by this function in and on itself. It's a nice way of separating the "dumb" UI code that does nothing else than the rendering from the logic that connects the UI with the store, that's perfectly fine. The issue lies within the fact that the connected component has an additional source of information (the store). If you have a junior developer using your component, they might not be aware of this fact.In 99% of all cases, it simply doesn't even matter, I give you that. It's mostly an "academic" concern. However, the moment you employ connected components in your application, you absolutely should not use
shouldComponentUpdate
anymore. The reason for that is that you can never be sure which child components your component at hand will have in the future. Today, your child components might be "dumb" react renderers (stateless components in the ideal case). However, tomorrow, in the third nesting level, someone might sneak in a connected component. From that point onward, yourshouldComponentUpdate
function will potentially prevent an update that was actually needed by a child. This introduces very subtle bugs that are extremely hard to detect by the developers because they seemingly occur "at random". At least that's what your users will tell you: "Sometimes this thing here doesn't update". And good luck in figuring that out. If you use react-redux, every time you implementshouldComponentUpdate
, you make the implicit (and very strong) assumption that all your children, now and forever, will be pure, non-connected components. There is no way to enforce that in the react API, and somebody eventually WILL break this assumption. Maybe you yourself because you haven't touched this code in a long time, or maybe the junior dev next to you, but somebody will. From the point of view of that person, they don't do anything "illegal", they just attach another component to a parent component.So, why bother, if you can just skip
shouldComponentUpdate
? Well, because it's a very important tool for optimizing react apps. React is fast in rendering, no doubt about it. However, if you are writing really large applications where a react component tree consists of hundreds or thousands of components, you will eventually need to optimize it, and you lose this very important tool.I'm writing react-redux apps myself. It's not a deal-breaker. But it is something that we as react devs absolutely need to keep in mind. It is a problem, and I'm not sure if there is a solution to this at all.
I think those are good points. There are always trade offs.
can you develop more the part when explaining why using redux, I get the problem, but the solution in Redux doesn't seem explained like the problem part.
I really loved you post ♥
Thanks for the comment Belhassen! Do you have any specific questions about the Redux solution? I would love to improve the explanation and your questions would be helpful to understand what is missing.
In the mean time this post may help you wrap your head around Redux: dev.to/ross/reduxjs-in-30-seconds-5hj
I meant like some examples with redux to solve the problem. thanks for the link ♥
This a really good explanation of React and why it needs Redux.
I have started to watch some tutorials related to Redux and they were like chinese to me until I found your article.
Love it! Please write more if you have the time and possibility.
Thank you Dumitru :)
Great article! Thanks!
I'm not sure there's less coupling or complexity when every component depends on the same global store... you might like this alt to redux as well.
dev.to/chadsteele/eventmanager-an-...