-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
allow definition of async thunks from createSlice #637
Conversation
src/createSlice.ts
Outdated
| ValidateSliceCaseReducers<State, CR> | ||
| (( | ||
creators: ReducerCreators<State> | ||
) => CR) /* & ValidateSliceCaseReducers<State, CR> */ // TODO: possible? |
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.
If this is not possible (which I'm not sure about), it would not be possible to test the result of the builder notation if the payload
of reducers created with the prepare
notation would be correct between the reducer & the prepare function.
=> Maybe we should forbid that definition here and add another withSpecial.reducer
function for usage with the builder notation? Would be more "in theme" anyways.
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've added create.preparedReducer
as a new method and throw an error if the prepare
notation is used without that in combination with the create
notation, as in that case we cannot correctly check if the payload returned from prepare
is the same payload as expected by the reducer.
It's unfortunate, but when I try, the inference breaks in all directions due to the type being inferred from a method return value in the first place.
On the other hand, it's probably more consistent anyways.
Deploy preview for redux-starter-kit-docs ready! Built with commit df00e1e https://deploy-preview-637--redux-starter-kit-docs.netlify.app |
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit df00e1e:
|
|
||
function getReducerCreators<State>(): ReducerCreators<State> { | ||
return { | ||
asyncThunk(payloadCreator, config) { |
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.
Just to check... were we going to try to make it asyncThunks
to allow multiple entries with only having to specify the state/extra types once?
Also, what's the need for having the payload arg defined separately?
Any chance something like this would work?
builder.addAsyncThunks<State, Extra, Whatever>([
{payload, pending, rejected, fulfilled},
{payload, pending, rejected, fulfilled},
])
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.
Something like
const slice = createSlice({
name: 'test',
initialState: {} as TestState,
reducers: withSpecial => ({
...withSpecial.asyncThunks<RootState, Extra, Whatever>({
thunk1Name: {payloadCreator, pending, rejected, fulfilled, options},
thunk2Name: {payloadCreator, pending, rejected, fulfilled, options},
}),
could potentially work, but type inference from the payloadCreator
to the pending
/rejected
/fulfilled
would be very flimsy - the prepare
notation at the moment often requires manual type annotation and only checks that nothing wrong was annotated; we'd probably be running into similar problems and very complex types that way.
Also, what's the need for having the payload arg defined separately?
The splitting up was necessary as otherwise in fulfilledReducer(state, action)
, action.payload
would always infer to unknown
and I didn't find another way around that.
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.
PS: also, the Rejected
value might be different between the thunks, making this even more difficult, as that can never be inferred.
e18e23c
to
76ae34f
Compare
Okay, aside from this breaking in older TS versions, I think this is pretty much where I would want this to go. @markerikson & @msutkowski, want to discuss this some more before I start to clean this up? :) |
Okay, so, lemme see if I have this usage right: createSlice({
name,
initialState,
reducers: create => ({ // callback form
fetchTodos: create.asyncThunk(
todosAPI.fetchTodos,
{pending, fulfilled, rejected} // thunk reducers here
),
toggleTodo(state, action) { // "normal" reducer in this object
state[index].completed = !state[index].completed;
},
addTodo: create.preparedReducer({
prepare: (text) => ({payload: {text, id: nanoid()}}),
reducer: (state, action) => void state.push(action.payload);
})
})
}) So:
I think my biggest questions are around naming and docs / teaching. What do we call this form? How do we teach it? How do we differentiate the behavior and naming between this and the "builder callback" form? (Oh, and I do really like the Great job as always, Lenz! |
@markerikson @phryneas I went ahead and just tried it out on that generic CRUD entity adapter thing as a quick demo for usage - https://codesandbox.io/s/create-asyncthunk-ex-y9n03?file=/src/crud-factory.ts |
Almost, it is: createSlice({
name,
initialState,
reducers: create => ({ // callback form
fetchTodos: create.asyncThunk(
todosAPI.fetchTodos,
{pending, fulfilled, rejected} // thunk reducers here
),
toggleTodo(state, action) { // "normal" reducer in this object
state[index].completed = !state[index].completed;
},
- addTodo: create.preparedReducer({
- prepare: (text) => ({payload: {text, id: nanoid()}}),
- reducer: (state, action) => void state.push(action.payload);
- })
+ addTodo: create.preparedReducer(
+ (text) => ({payload: {text, id: nanoid()}}),
+ (state, action) => void state.push(action.payload);
+ )
})
}) As inferring a type from one argument into a second is a lot more simple than doing so within one object argument. Also, IDE type hinting seems to work better that way. (Same reason why we have that split in That said: it might be easier here than elsewhere. If you prefer that other notation, I can give it a try.
exactly 👍
Yeah. Writing/adjusting the docs for this is my personal nightmare to be honest 🙁
nice 😄
🙃 |
Random question: will it possible to create a action creator thunk without using createAsync? There are cases where you don't want all of the pending/rejected/fulfilled actions, but you do want to be able to arbitrarily dispatch actions within an asynchronous process. |
@nathggns you've always been able to define thunks outside of If a handwritten thunk is defined outside, then it can use action creators that were generated inside const todosSlice = createSlice({
name: "todos",
initialState: [],
reducers: {
fetchTodosStarted(state, action) {},
fetchTodosFailed(state, action) {},
fetchTodosSucceeeded(state, action) {
return action.payload
},
}
});
export const {fetchTodoStarted, fetchTodosFailed, fetchTodosSucceeded} = todosSlice.actions;
export default todosSlice.reducer;
export const fetchTodos = () => async dispatch => {
dispatch(fetchTodosStarted());
try {
const response = await todosAPI.getAll();
dispatch(fetchTodosSucceeded(response.todos));
} catch (err) {
dispatch(fetchTodosFailed(err));
}
} The problem is that
So we specifically want to add syntax to support the case of defining thunks with |
@phryneas so where does this stand atm? I'd really like to push this through in the near future. |
Yeah, I just pushed https://github.com/reduxjs/redux-toolkit/commit/df00e1ea24709faf304932909f7f0159ef7f0002 which showcases a circular type reference problem and the solution. Please take a look. If we decide that we can tackle that in documentation, good. If not - I'm not sure if we can merge this feature at all then. |
2340a6d
to
df00e1e
Compare
I have a few thoughts here that ultimately boil down to: this is a good API for JS users for sure, but the additional overhead/downsides for TS users would make me question recommending it. Downsides:
We could possibly get around those downsides by explicitly not recommending that syntax for TS users, but I'm not sure that's a good idea. We put notices about the Usage with TypeScript guide everywhere and lots of folks still skip that. |
I really, really want to be able to define thunks inside of Unfortunately, as we're seeing here, the issue is TS definitions vs trying to infer things. If the I don't have any suggestions atm, but I sure hope we can figure something out... :( |
Well, I have to say that I would really like to get this out as well - funny enough, not even necessarily for the So I believe it comes down to a matter of teaching. If we keep the pattern One point though: I would like to suggest that we do this in a major release and get some other stuff in (and some out). My suggestions would be:
This will also require quite some work on the documentation as well, so it would not be a "release tomorrow" thing, but I believe we could get this done in... dunno, two months? We could also target the birthday of RTK as a release for 2.0 ;) I could also try a few of my friends and colleagues if one of them would be willing to pair up on some of the documentation stuff, that could help things go forward, we can probably need a few more hands for that. |
Hmm. Lot of stuff to unpack there. Quick thoughts:
So... I kinda see where you're going with most of this, but that's a pretty big list of changes :) |
Yes, that would be the downside to that. Personally, I believe this would be worth it - stuff like #680 is just too obscure and I'd really root that out.
It's the definitive way to prevent that circular import problem when using
Primarily: teaching. It's a bit like the
Yup :/ But it's kinda getting inevitable to do a partial docs revamp at this point.
Yup, but on the other hand: if we're doing a major release (and we should, we are getting quite far away from the initial 1.0 anyways), it's gotta have some goodies ;) By the way: the person I had in mind for helping me is up for it, so I have someone poking me in real life to do something, which always helps ^^ |
Of course we can always decide to cut down on the scope or just release some minor things while delaying a major version, but I'd suggest planning kinda big for now :) |
To add to that list when re-evaluating |
Umh yeah, I meant to write But yes, we should also take another stab at |
This seems to be A) unworkable, and B) semi-obsolete due to the upcoming RTK Query addon. As much as it'd still be neat to have this, it doesn't seem like that's going to happen, so I'm going to close the PR. |
This is still a WIP, but it's getting complete enough to collect some feedback.
createSlice
itself started to become quite complex, so I did some refactoring & split the reducer handling out.