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
Continuation of #698 - Redux time travel #701
Comments
I can't really see why that would happen. That action seems to only get fired by unsubscribing to the query -- are you maybe doing that?
Are we talking about nested graphql components here? I could see that happening maybe, but I'm not totally understanding how that flow could come about otherwise (why would the component re-mount on query results otherwise?) If it is the nested case, it's a bit of a doozy. I wonder if you have any ideas about how we could resolve this? |
Ok, so if
I didn't expect the query to be subsribed even after it was fulfilled. I thought after
It's not nested components. It's a react native app and there's navigation involved. When navigating from the login screen to the home page (connected with When I use Redux DevTools to play around with my store I start seeing the weird behavior. (e.g., |
By the way, would you say nesting GraphQL components is an anti-pattern and that there should only be one top level query that gets everything and passes down the props as needed? I originally thought I could take advantage of the cache and nest the components and the nested ones wouldn't have to do duplicate queries, but it turns out that doesn't work as well as one would expect. |
RA needs to keep watching the query so later results to the query (from mutations or polling or whatever) can be reflected in your UI.
Oh, so you are saying other redux actions you time-travel through are causing the component to get mounted? I guess that's more or less the same problem as the nested queries one if you think about it. I wonder if dispatching actions from component lifecycle is an anti-pattern in Redux. It seems like you'd inevitably run into this problem when time-travelling. Although I've no real idea how you'd do it otherwise. /me is going on a google journey now. |
No, I don't think so!
Tell me more about this. I don't think these patterns have been completely settled yet. |
Yes, a case would be a simple nav push, like
The only thing that comes to mind in this case would be to go extreme redux and simply dispatch a thunk (or start a saga) when the component mounts and then let it handle asynchrony from there and fire the actions that change the store as it goes. This still would happen automatically when the component mounts, though, so it really doesn't sole our problem. The only way to prevent this would be to have some sort of "time-travel mode" where thunks aren't added to the time travel list of actions... and actually, now that I think about it, I think that's the case.
Of course! 😄 So I'm building a production React Native app that uses GraphQL and Apollo. I'm running into a lot of real-world use cases so I wouldn't mind sharing what I've come through/learned. For example, I know you're still missing a Relay example in the docs for pagination in Anyway, to answer your question. Sometimes nested components can get the necessary props from the parent but other times the queries are completely different and the caching system won't help you. For example, if you have a query called Another thing that I haven't looked much into is that I think that the nested components mount first (before their parent components), so it becomes awkward to fetch a lot of fields from the nested components simply to cache them. Anyhow, as I keep building the app I'll keep finding better ways of approaching each issue. I hope this is helpful. |
Is that true? (That TT ignores thunks). What is the mechanism for that? I wonder if there's an equivalent approach we can take.
That'd be awesome!
That makes sense; there's not really any way around that. I do wonder what the use case is for a separate query for the comment however (I am imagining a list of comments, but maybe I'm off base here?).
Wouldn't the nested component depend on the result of the parent component? I would have thought if that is the case they would have to wait on the parent's query. But maybe we need to be more concrete in the example.. |
I'm sorry for using the wrong nomenclature here. The Redux DevTools store enhancer will only track dispatched plain actions (POJOs), meaning that thunks (functions) will not get recorded. I'm not 100% on this but I'm pretty sure I read about it a while ago. I tried to find the source but no luck. The idea is similar to putting
Imagine having a feed of items, lets say tweets. The query to get the feed is analogous to
You're right, the child component probably the parent to be there to be able to mount in the first place. |
Forgot to mention, I edited my comment above. Not sure if you saw the edits. |
By the way, if you want me to, I'd be happy to go into more detail into what I mean by having the query logic live in the Redux side. |
Re: nested queries: I think the better approach here is to use a fragment in this case (where you want to separate the concerns about the structure of the comment query). Then the feed can grab the fragment off the comment and include it in the query. Our fragment support is pretty raw and we don't really have great patterns for this but it's something we actively want to explore and figure out a solution to. Probably something less intense than what Relay does is where we'll end up.
The part I can't figure is how if your component dispatches in its lifecycle callbacks (via thunks or otherwise) how you could avoid extra actions (forget network requests for a moment) occurring when the component is rendered during TT, unless there's some special mechanism where Redux stops dispatching or something. I too need to do a lot more research in this area but it's something I do want to look at in a lot more detail soon. I'll report back as I figure stuff out. |
You're right! Fragments are exactly what I need.
Have you seen what lokka does for fragments?
You're exactly right about Redux having to stop the dispatching. Basically, "time-travel mode" could be activated by dispatching an action |
I know this is completely outside the scope of Apollo now, but I'm enjoying the discussion! 😄 |
If I was implementing this from scratch I would do the first half of what you said but when replaying actions attach an extra |
Redux DevTools actually doesn't do that. Maybe it's something they should implement? It sounds like that should be the functionality out of the box given that when you're time traveling you wouldn't want to dispatch extraneous actions. |
Pinging @zalmoxisus and @gaearon. |
The extension provides a UPDATE: I just read that it's about React Native. So you're probably using Remote Redux DevTools?
Yes, we want to implement something like this to lock all the changes while time travelling. Would that help? |
@zalmoxisus, thanks for chiming in! I think that exposing I guess my question is, do we want the dev to handle this or should there be a switch in the DevTools monitor that when on will simply ignore any actions not triggered by the monitor? What I mentioned above, regarding ignoring anything not a POJO, doesn't really work well because a POJO sent in componentWillMount can still trigger side effects (i.e., via middleware). The correct approach would be to simply ignore any action that doesn't come from the monitor, given a certain predicate (i.e., EDIT: And it turns out you already expose such predicate! 😄 |
Also, I really love the work you've been doing to handle errors remotely and in realtime as they happen in production. It would be impossible to do this though without suppressing side effect actions from EDIT: For the remote user that's getting their app debugged, once time travel mode kicks in, they basically wouldn't be able to do anything in the app (as long as all app interactions are handled by Redux). |
@migueloller, thanks for the feedback!
Redux DevTools enhancer knows nothing about action creators. It recomputes only reducers. That said, if the dev calls an action creator, for example from |
Yes, being able to just drop all actions that don't come from the monitor would be awesome! It would allow for time travel without having to worry about actions springing up from Thanks for the prompt responses! |
@migueloller, I've just published store.liftedStore.dispatch({ type: 'LOCK_CHANGES', status: true }); To unlock changes: store.liftedStore.dispatch({ type: 'LOCK_CHANGES', status: false }); Please let me know whether it helps. |
Seems pretty great! My naive thought (and I'll admit I haven't spent a lot of time thinking about this), was that when the user time travelled, the TT mechanism would automatically lock dispatches until rendering is done (maybe a small timeout to be safe). Then if the component sticks to dispatching single actions (which could be picked up by something like redux saga), or thunks, then it will be "locked out" during React's component lifecycle and everything should work. If the component doesn't follow that pattern, and instead initiated network calls / something else that dispatched actions after timeouts (this is what Apollo is doing right now I think), then it wouldn't work; but I suspect the above way is more idiomatic Redux anyway, right? |
@zalmoxisus, awesome! Definitely helps. Now I'm just waiting for the button in the monitor so that I don't have to use the dispatcher. 😉 @tmeasday, yeah, it looks like blocking all actions while time traveling is the best way. Thanks all for the great discussion. I'm satisfied with the outcome and am going to close the issue. 😄 |
@migueloller is there still an issue that Apollo dispatches actions in the "wrong" way and this mechanism won't (for instance) stop network requests when you are TT-ing? |
@tmeasday, nope. Apollo never did anything wrong really. I just got confused with actions getting fired on The one thing that could be "wrong"/annoying is all the Does dispatching |
@migueloller but am I not correct in saying when a
Aside from the unnecessary network requests, it seems like that I would have thought the
It removes the query status from the store. In the future you could imagine it might remove the query results or mark them to be GC-ed. |
@tmeasday and @zalmoxisus, Ok, so there are 2 different things we're talking about here. The first one is what @zalmoxisus just mentioned which is to be able to lock all changes to be able to work on some feature. From seeing this line though, it looks like it's not made for time travel, but more to simply lock the entire store so that nothing happens. So it looks like this specific change won't solve our problem. The second is one is what @zalmoxisus had mentioned in a previous comment regarding the @zalmoxisus, do you see how these are 2 different use cases? I could even potentially see 2 different buttons in the monitor for them. One to drop all actions and another to drop all actions that don't come from the monitor. @tmeasday, to answer your question: If that is the way that Apollo currently handles it, then yes, it will interfere with time travel. But this will only happen if the store is unlocked (i.e., accepting non-time-travel actions) when the result comes back. This is unlikely. It is possible to make it 100% foolproof though, doing it with super idiomatic Redux. To make it so, it would be necessary to have a custom Apollo middleware that initiates the network process. See redux-api-middleware for an example. With middleware like that, the action that calls the API would never be dispatched, so the network request wouldn't be made and the result wouldn't be a problem. I think that if Apollo implemented something like this internally it would definitely be cleaner. Then basically, what a connected GraphQL component does is dispatch an action when it mounts, it doesn't even have to worry about unsubscribing when it unmounts because all of that happens internally in Redux land. |
@migueloller, could you please elaborate why |
In case of time travelling, |
DevTools enhancer is the last in the compose, and all the middlewares are invoked before it. We need the second enhancer at the beginning. I've just published However, I guess you want to prevent invoking network requests not from middlewares (as it would be in case of Update: What if we'll expose a global variable like |
I apologize, I thought that |
@zalmoxisus I'm finally caught up on this. I tried using the I created a really simple example that tries to simulate how
I definitely see issues when I time travel around in that because replaying actions makes the left pane appear and disappear, and new subscription and teardown actions occur. Locking does not appear to stop that happening -- it does however seem to lock the UI. Is this how you imagine a dev-tools compatible Redux based network layer might operate? Is there a better way to think about it? [1] Code is here, there is a bit of other |
@tmeasday, thanks for the example! I've just tried it, and see the issue with time travelling, but everything works fine when it's locked (after Am I missing something? |
@zalmoxisus you know what, you're right! I don't know what I did before, but when I tried again it worked exactly as I hoped. Great! I think we are on the same page then. So would your opinion be that driving network operations via thunks is the way to go? It seems like it to me. |
Thunks would be ok, as well as other middlewares. You could also have your custom middleware like in the real-world example. Locking will block all middlewares and store enhancers. In case of having network request from In future we could autolock the store while timetravelling. Thanks for investigating this! |
Locking was implemented in Redux DevTools Extension and in Remote Redux DevTools. Here's a blog post with more details. |
@tmeasday, thinking over, I don't think we should force users to add function clientMiddleware(store) {
return next => action => {
const result = next(action);
// ...
if (action.type === 'SUBSCRIBE_REQUEST') {
// network requests or other sideffects
// here we can dispatch also: next({ type: 'SUBSCRIBE_SUCCESS' })
// or: next({ type: 'SUBSCRIBE_FAILED' })
} else if (action.type === 'TEARDOWN_REQUEST') {
// network requests or other sideffects
}
return result;
};
} And from the component: componentDidMount() {
this.props.dispatch({ type: 'SUBSCRIBE_REQUEST' });
}
componentWillUnmount() {
this.props.dispatch({ type: 'TEARDOWN_REQUEST' });
} |
Yes! That is the way to go. |
This issue has been automatically closed because it has not had recent activity after being marked as stale. If you belive this issue is still a problem or should be reopened, please reopen it! Thank you for your contributions to Apollo Client! |
@tmeasday,
I went back and played around with time travel a bit. What seems to be popping out of the blue when I reset my store are
APOLLO_QUERY_STOP
actions. I'm not sure if these should be happening or not. Maybe you can shed some light on this?Nevertheless, I'm realizing that time traveling a
react-apollo
app isn't really possible because data from thegraphql
higher order component gets called oncomponentDidMount
. This means that as you try to reproduce each step, once you get to the step that would mount the GraphQL connected component, the queries are going to fire off, thus overlapping with the next actions one would manually dispatch while time traveling.It's definitely possible to reduce the state from all the actions though. 😄
EDIT: Starts where this comment left off.
The text was updated successfully, but these errors were encountered: