-
-
Notifications
You must be signed in to change notification settings - Fork 10.6k
Playing nicely with redux, mobx and pure components #5076
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
@mjackson I thought at one point near the end of 2016 you said that the team was exploring using a subscription model parallel to what react-redux does. It has probably been rehashed quite a bit before but are there perf reasons this wasn't done? I agree with @d-leeg that needing to guarantee the location prop on arbitrary ancestor components if they are ever I'd love to be educated though, I know there is probably a lot of history on this issue I am ignorant of. |
I agree with @d-leeg. I'd also like to add that, in my opinion, to improve the interaction with dataflow libraries like redux/mobx the query should be stored as an object. Storing and mutating the search string makes it very difficult if you use different query parameters in different components in your app, since they would all be rerendered if anything changes in the search string. Implementing shouldComponentUpdate is difficult, and manual query string parsing everywhere wastes performance. The main reason for removing the query string was "We've had many, many requests over the years to modify the way we handle query string parsing and serialization." (#4410), so I propose that we return the query object and introduce |
@etienne-dldc Thanks for the reference. The question/request raised by this issue is how to nicely co-exist with libraries like redux and mobx. Perhaps a subscription mechanism like I suggested may be offered as an option for users of these libraries. Now, to directly address that answer: looking at the two reasons given for why subscriptions were abandoned, I have addressed both in my proposed solution: the first (stale context) is completely eliminated since the context never changes, and the second (unnecessary-re renders) can most probably be addressed without a scheduler by simply caching the last location that was received. |
@d-leeg Um... The way I understand the article is that they were unhappy using "tricks" (subscription mechanism) so they removed it. But I might be wrong ¯_(ツ)_/¯ . Also I totally agree with you about the drawbacks of current RR4. In fact, to solve this problem I have made a custom version of RR for one of my projects that do use subscriptions and the reason I end up here is that I was searching for why RR4 is not using subscriptions and can we avoid making "yet another routing library" for those who want a subscription mechanism. BTW if you're curious about it you can find my implementation here : Realytics/react-router-magic. |
@etienne-dldc it looks like we are basically in agreement (however I will not call the observer pattern "a trick") :) I will definitely look at your code later this week. Thanks for sharing! If you look closely at my suggested solution, it also cascades. The It would be great if we can get some feedback from the main contributors... |
I stopped using React Router v4 because of these necessary "tricks". But I loved its API, I hope to see a solution soon. |
I would say version 4 is broken by design. React documentation says don't update the context. Quote from offical React documentation. If you should update context it has to be through "observers".
https://facebook.github.io/react/docs/context.html#updating-context |
If anyone is interested, the way I now deal with routing is the following:
This approach have been quite successful so far, the main advantage is that routing is done in redux so you can do some nice optimisations 😄. |
I would like to open a discussion concerning the way react-router plays together with
PureComponent
and libraries likeredux
andmobx
.This topic was briefly touched upon in #4680, but I believe that due to the fact that it influences a huge number of developers, and that the workaround suggested there leaves a lot to be desired; a more thorough discussion is warranted.
I wish to start this discussion here by considering 4 points:
To recap, the heart of the problem is that
react-router
uses thecontext
to pass downlocation
andmatch
information. However, changes to this information areblocked, and not propagated down any sub-tree of the components tree that is rooted at a pure component that has no interest in the
react-router
context
.Obviously, such blocking completely breaks down the functionality of components such as
Route
in the sub-tree.Unfortunately, blocking components are rife: either due to explicit use of
React.PureComponent
, or by using libraries such asreact-redux
andmobx
; for example, everyreact-redux
component created using a regular call toconnect()
is pure. As a result, the solution suggested in the guidelines, of punching a hole in every blocking ancestor by obtaining the location and passing it as a prop, is far from being ideal, both from a practical point of view, and from a more conceptual point of view.More specifically, it suffers from at least the following drawbacks:
Punching the holes must be done explicitly (i.e., it is not automatic and transparent).
It potentially causes many unnecessary re-renders: a punched component high up the tree will try to re-render the whole sub-tree on every location change.
It completely ruins modularity. This is undoubtedly the biggest issue, and for medium sized and larger projects that use
redux
it is actually a killer. The reason is thatwhen an affected component (such as a
Route
) is added anywhere in the component tree, all its blocking ancestor components must be punched. In other words, a change toone component requires a change to multiple other existing components that need not even be aware of its existence: modularity is dead. This is a very big problem for reusing code
and, in projects where different teams are responsible for different components, it is almost intolerable. The only way to regain modularity is to have all components punched
in advance. This is clearly unacceptable as it results in a full-scale elimination of the concept of pure components.
Other libraries (such as
react-redux
) do not suffer from such anomalies. E.g., pure components do not block nested connected components from having access to the information they need.From a conceptual point of view, why does react-router use the context? The obvious answer is in order to not have to inject unneeded props to ancestors. The proposed
solution of punching holes by passing unneeded location props to blocking components obviously negates the whole idea of using the context in the first place.
I will now describe briefly what I see as the source of the problem.
Please forgive me if I am stating the obvious, or making mistakes. As Michael Jackson and Ryan Florence said in their talk at ReactJS 2016, we are all still learning how to use React.
At its core, the problem is that pure components, that do not care about the
react-router
context
, do not pass the newcontext
down when it changes.This is definitely a conceptual problem within React, and I guess is at least part of the reason why the context is still considered experimental by the React team.
However, this does not pose a problem if the information passed in
context
never needs to change. For example, inreact-redux
the redux store (more accurately, a pointer to it)is passed down the
context
, and this pointer never changes. Being a mutable object (observe that the store, unlike the state of the store, is mutable) one does not have to changethe pointer to the store in order to pass down the new state of the store. The latter is always accessible by calling
store.getState()
. On the other hand,react-router
passes in thecontext
immutable data (namely
match
andlocation
insideroute
), and there lies the problem.I suggest the following solution: have any
react-router
component, likeRouter
orRoute
, that passes information down in thecontext
never change that information!Thus, for example, instead of passing down
route
, which needs to change as thelocation
(andmatch
) change, pass down a constant functiongetRoute()
that returns the most up to dateroute
.Let's call such a component
X
. The only missing piece now is to make sure that a client component (likeRoute
) which is a descendant ofX
, will be notified wheneverX
has newroute
for it. This can be easily done using a subscription mechanism (like inhistory
orreact-redux
).More concretely, this will result, for example, in the following changes to
Router
: remove route from the childcontext.router
, and add asubscribe
andgetRoute
methods instead.The changes to
Route
will be as follows: as forRouter
, removeroute
from the child context, and add asubscribe
andgetRoute
methods instead. In addition,in
componentWillmount
subscribe a listener tocontext.subscribe
(and unsubscribe incomponentWillUnmount
). This listener will callcontext.router.getRoute()
,use the returned ancestor
route
information to set its ownlocation
andmatch
,and then notify all of its listeners of any changes. In addition, anywhere
context.router.route
is used, it should be replaced withcontext.router.getRoute()
.Alternatively, one can get rid of
getRoute
altogether, and pass down anyroute
changes to the listener callback, which will then cache that information for future use.Observe that the above solution will easily traverse any blocking components, with no ill effect, and completely transparently.
The only drawback I can see (apart from the negligible overhead) in a naive implementation of the above, is that it may cause double rendering of nested
Route
components that are not blocked:. I.e., when the location changes, first the ancestorRoute
/Router
will start a re-render that will cause the descendantRoute
to re-render, and then the subscription mechanism will kick in triggering another re-render when the nestedRoute
's listener method gets notified. However, This can be easily circumvented (as is usual in React) by shallowly comparing the currently cachedroute.location
androute.match
with the ones in the newroute
in the notification.Here it is guys, what is your opinion?
The text was updated successfully, but these errors were encountered: