-
Notifications
You must be signed in to change notification settings - Fork 31
Conversation
@willdurand @bobsilverberg let me know what you think of thunks! I added questions and known issues to the PR description. |
Codecov Report
@@ Coverage Diff @@
## master #176 +/- ##
==========================================
- Coverage 94.77% 94.09% -0.68%
==========================================
Files 20 20
Lines 287 305 +18
Branches 71 74 +3
==========================================
+ Hits 272 287 +15
- Misses 14 17 +3
Partials 1 1
Continue to review full report at Codecov.
|
It is a dumb question but: isn't it that |
This could be done by dispatching an action at the beginning of the thunk as shown here: #12 (comment). AFAIK you did not like that an action intercepted by redux-saga was automatically replayed either. I think we have the best of both worlds if we use thunk + the pattern that consists in dispatching an action early in the thunk. |
How would that solve it? When replying actions, you would just be replaying the first action, not re-executing the entire thunk function. |
Yes, that is correct and that is the problem. A thunk is a function, not an action, so if you edit the code in this patch to do |
I am not sure to follow. You want the execution of the thunk to be captured. What do you expect precisely? I read it as "I want to know that the thunk has been executed". Which other information is missing otherwise? |
The middleware changes the behavior so that you can dispatch both, so why do reflecting that in the type? |
src/components/Navbar/index.spec.tsx
Outdated
creator: jest.fn().mockReturnValue(dispatchCallback), | ||
dispatchCallback, | ||
}; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this would be easier to follow if we had different names:
const dispatchCallback = '__dispatchCallbackReturnedByTheThunk__';
const _requestLogOut = jest.fn().mockReturnValue(dispatchCallback);
expect(dispatch).toHaveBeenCalledWith(dispatchCallback);
I don't really understand the API of the object returned by createFakeThunk
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what to add except that I too find this test confusing. Maybe there's a way to make it clearer. I think @willdurand's suggestions would help.
src/reducers/users.spec.tsx
Outdated
dispatch, | ||
thunk: () => thunk(dispatch, () => store.getState(), undefined), | ||
store, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this helper function!
@@ -11,6 +11,7 @@ describe(__filename, () => { | |||
}; | |||
|
|||
const wrapper = () => { | |||
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this resolves #174 :D
I expect to preserve this key principle of Redux as much as possible:
When not relying on side effects, this principle can be achieved by simply dispatching actions:
We can replay these two actions and execute the exact code that led us to these UI states. We cannot replay the click handler functions which dispatched each action but as long as those handler functions do nothing but dispatch actions then the risk of breakage there is minimal. Let's say we needed to rely on side effects and we implemented them with sagas. Dispatching actions would look more like this:
We can now replay the action Here's how it would look with a thunk:
We can replay the I'm going to try |
Thanks for the detailed reply, now I get it.
As per #12 (comment), this is exactly what you wanted to avoid though:
That being said, I understand that replaying a thunk is difficult but given a state and a stack of actions, you will still be able to reproduce what has happened, without firing the API call. I mean, in terms of data flow, you would get the same end state. Unless there is an issue with the The redux devtools allow to load a state and to replay a stack of actions, so if we can record the stack of actions, we could also retain the initial state and either go forward or backward (if we can capture the end state too for example) |
Yes, that's true. It's because I mostly want to use the developer tool to develop screens and sometimes I like to hop around between states. We had the habit of rendering important screens using the same action that triggered a saga and that made it hard to time travel. You could do it but the stack of actions would keep growing and growing which made it hard to move forward after moving backward. You raised a good point in that conversation about how you do want to see side effects in the developer tool. I get that, too. Ideally, I'd like a checkbox to temporarily disable side effects.
Yes, but all of the code executed in between is very crucial to the program flow, especially in trying to track down unexpected errors. The same would go for rendering a component: when replaying an action I would very much want to see an error thrown by I would like to store a stack of actions as Sentry breadcrumbs one day 💡 In dev / stage only, of course! 😎 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks pretty good to me, thanks @kumar303. It feels a bit weird to have the thunk code right in the reducer - I guess I'm used to having the sagas in their own files - but I think it's okay.
I would be fine with us taking this approach, but if you're also going to do an example with redux-loop
I'd love to see that as well.
src/components/Navbar/index.spec.tsx
Outdated
creator: jest.fn().mockReturnValue(dispatchCallback), | ||
dispatchCallback, | ||
}; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what to add except that I too find this test confusing. Maybe there's a way to make it clearer. I think @willdurand's suggestions would help.
src/components/Navbar/index.tsx
Outdated
dispatch(userActions.logOut()); | ||
logOut = () => { | ||
const { _requestLogOut, dispatch } = this.props; | ||
(dispatch as ThunkDispatch)(_requestLogOut()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw your comment about this and yes, it would be better if we could find a way to not have to do this type hinting. It seems like the component dispatching the action shouldn't even need to know if it's a "plain" action or a thunk action.
04f4e64
to
c41bea7
Compare
Not dumb at all! The declaration of |
@bobsilverberg @willdurand I think we should go with thunks. I still dislike how thunks are not traceable given actions alone but since they don't require generator functions, they are probably our best option, IMO. Could you take another look at the patch? I have abandoned my attempt at getting redux-loop to work (#229). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this patch, especially the testing part, thanks! That works for me.
* | ||
* You can make an assertion that it was called like: | ||
* | ||
* expect(dispatch).toHaveBeenCalledWith(fakeThunk.thunk); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️
Thanks for the review. @bobsilverberg I'm going to merge this to keep it going but let me know if it needs any follow-ups from your point of view. We can always revisit this side effect strategy as we begin using it more. |
Fixes #12
After some consideration (see below), redux-thunk is a pretty good option. This adds redux-thunk for handling side effects.
Original PR description:
As part of #12, here's an example of using
redux-thunk
so we can assess it.Here are some questions worth asking:
Known issues:
The(dispatch as ThunkDispatch)
type hint seems like it should be unnecessary since we have types for our middleware (see Use with TypeScript 2.0 (@types/redux-thunk) reduxjs/redux-thunk#103) but I couldn't get the error to go away without this type hint, even when doing the same module override.requestLogOut()
is not captured in action replay. When we used sagas, it was possible to replay the stack of actions and re-execute each saga. This reduces traceability. As an example, let's say we were given a stack of actions by someone who encountered an error thrown in a thunk. The stack of actions would not let us reproduce that error.