-
-
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
Setting "meta" on the action generated by "createAsyncThunk" #750
Comments
At the moment, no, there isn't, beyond the fact that whatever you pass as an argument to the thunk action creator when you dispatch it will wind up as What specifically are you needing to add to the |
Thank you for your reply. Here is the interface of the meta object that i generally use if I'm using createAction or a regular action. interface Meta {
throttle?: number;
debounce?: number;
}; // createAction, simple example for how the action looks like for the middleware
const action = createAction( "someActionType", ( data:Record<string,any> ) => ( {
payload: data,
meta: {
debounce: 1000
}
} ) ); Debounce middleware is really useful when making a hit to search api end point with some debounce time, waiting for user to stop typing in a search field. |
I was just looking for something similar, but not at the point of dispatch, but rather in |
The keys need not end up in fulfilled, pending, and rejected actions. Because i don't want to I can us it in below fashion to achieve throttling. // throttle.tsx
// ----------------------------------------------------------------------------------------
// variables
const throttled: Record<string, boolean> = {};
// ----------------------------------------------------------------------------------------
// function
/**
* throttle function to be applied on condition callback for createAsyncThunk
* @param name unique name to identify the action
* @param duration number
*/
function throttle ( name: string, duration = 1000 ): boolean {
// dont fetch, if already throttled
if ( throttled[name] ) return false;
// set throttled to true
throttled[name] = true;
// set the timeout to change the throttled back to false
setTimeout( () => throttled[name] = false, duration );
// continue to fetch
return true;
}
// ----------------------------------------------------------------------------------------
// exports
export {
throttle
}; // something.actions.tsx
// ...
const action = createAsyncThunk( "someActionType", async ( arg, thunkApi ) => {
// ...
// ...
}, {
condition ( arg, api ) {
return throttle( "someActionType", 1000 );
}
} ); I can't do the same for debouncing because the action has to wait (reset wait time, if action gets dispatched while waiting). And the logic for it is not as simple as returning It would be easy to implement debouncing using a middleware. Here is the middleware implementation, import { Dispatch, AnyAction, MiddlewareAPI } from "redux";
import { RootState } from "../storeConfiguration";
type PayloadAction<P = any, T = string> = Action<T> & {
payload: P;
meta?: Meta;
};
// ----------------------------------------------------------------------------------------
// variables
const timeout: Record<string, any> = {};
// ----------------------------------------------------------------------------------------
// middleware
export default ( { dispatch }: MiddlewareAPI<Dispatch<AnyAction>, RootState> ) => ( next: Dispatch ) => ( action: PayloadAction ) => {
// extract the debounce duration
const debounce = action.meta?.debounce;
// if debounce flag not enabled, forward the action to next middleware
// zero is falsy
if ( !debounce ) return next( action );
// clear timeout for the action
clearTimeout( timeout[action.type] );
// set(first time)/reset timeout for the action
timeout[action.type] = setTimeout( () => {
// delete the debounce key, if action gets dispatched again in the upcomming middlewares
delete action.meta?.debounce;
// delete the timeout in the timeout object (optional)
delete timeout[action.type];
// forward to next middleware
next( action );
}, debounce );
}; It would be really great if i could achieve this with |
Being able to set keys on Thank you, will appreciate your thoughts on it @markerikson |
FWIW, I can see some potential use cases for being able to define |
That sounds promising - you were thinking specifically for generating meta, or getting passed existing the existing meta (so you could add custom properties?) |
Well, that's kind of the question - I'm asking what your ideas are for the API and what specific needs you have :) |
Haha - just was a bit unsure since it jumped into the existing issue and I’m not sure our needs matched up to his.
Thinking about it I’m not exactly clear if there’s a general solution here. Am I correct that because the body of the async thunk is opaque, a ‘prepare({...})’ would have to add its properties before or during ‘pending’?
In my scenario I was looking to pass on the request url. I suppose if prepare took in the same ‘arg’ we could deconstruct it to generate the url.
I guess I would lean toward a prepare function that takes an options obj that includes nanoid, arg, etc. so if you do provide it, you are responsible for returning an object for meta.
(Excuse the formatting, on phone here...)
...,
condition: (arg) => {},
prepare: ({ arg, nanoid, ...}) => ({
requestid : nanoid(),
url: ...
})
};
… On 5 Oct 2020, at 16:07, Mark Erikson ***@***.***> wrote:
Well, that's kind of the question - I'm asking what your ideas are for the API and what specific needs you have :)
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
One way would be to have But should this Implementation might look something like this, function createAsyncThunk(typePrefix, creator, options){
// ...
// ...
function action(arg){
return function thunk(dispatch, getState, extra){
// ...
const meta = options.meta?.(arg, {extra, getState});
// ...
return Object.assign( thunk, {
meta
} );
}
}
// ...
} Instead of just returning the What do you think? I think i accidentally closed the issue, sorry. |
Next question is, should this callback be run for all 3 action types?
I'm sorry, that statement doesn't make sense. Thunks are functions. They don't have a What exactly are you trying to describe there? If you're talking about trying debounce the action creator itself, your best option is to do something like: const debouncedDispatchMyThunk = useMemo(() => {
return debounce(() => dispatch(myThunk())
}, []) |
Yes, I get that. I am referring to the function in the snippet. It might just be a function, but sill it goes through all the middlewares till it gets called by the As I said,
But I realised, the fact that its a function does not restrict us from having these properties on them. |
Lemme phrase it another way: what would you expect |
As of now, it might be specific to this case. You are right though, I could achieve it with the useMemo snippet, if this does not have any other use case. Only problem is I won't have access to the promise returned by the dispatch, to call Coming to |
Yep, passing Perhaps something like...
and then the internal usage would be like: const pendingType = typePrefix + '/pending';
const pending = createAction(
pendingType,
(requestId: string, arg: ThunkArg) => {
let extraMetaProperties = {};
const originalMeta = {arg, requestId}
if (options.metaCallback) {
extraMetaProperties = options.metaCallback(pendingType, {...originalMeta}, thunkApi);
}
return {
payload: undefined,
meta: {...extraMetaProperties, ...originalMeta}
}
}
) Would likely take some internal reshuffling to make that happen, and I'll admit it's a lot of extra work to go through, but I can see a potential point to it. Thoughts? |
Yes, that's great. May be |
I have an async thunk action to fetch multiple groups, allow me to set multiple keys in meta can avoid duplication of API calls easily, for example, group with id 2 should not be fetching if another thunk action is already fetching groups with id 2, 3, 4 |
@joseph1125 but that would be about accessing data in the action, not data attached to the thunk action creator. Totally different things. |
If you consider a universal loading store, this will make sense as keys in payload may have a different name |
I'm afraid we're really not communicating well here. When I do: const fetchUser = createAsyncThunk(/* */);
// later
dispatch(fetchUser(123)) the thunk function that is dispatched:
It's a function. It will be intercepted by the thunk middleware immediately, and not passed any further. For that matter, So, I was responding to this comment:
Which still does not make any sense - there's no reason to attach metadata to the thunk function itself. |
Actually, I think this feature would be very helpful because it would allow us to catch the actions in middlewares. It would be great if the custom meta-object were added to every phase of the action(pending, fulfilled, etc.) I suggest adding some kind of |
I'm trying to take a bit of a look at this now, and I really need some concrete examples of what information people would want accessible to calculate Like, it's straightforward enough to add support for an optional callback that accepts, say, But, it sounds like there's really a desire for dynamically generating some fields for
So, I need some idea of what parameters would potentially be useful for people to calculate whatever it is you want to calculate, so that I can figure out if we can make that info available. I'm inclined to say that we can't pass through So, is there anything people are wanting to see besides |
Not getting any responses so far, including after I asked on Twitter yesterday. I'd like to provide something for this in 1.6, but atm I don't feel I have enough info to nail down a solution. |
Still no responses on this, so I'm going to remove it from the 1.6 milestone. I'm still interested in adding this, but I'm not going to block the 1.6 release just because no one's giving me feedback on what use cases this feature needs to solve. |
You could for example use this feature to do optimistic updates for only a bunch of actions (e.g. Kanban Board Drag & Drop) const optimisticPatch = createAsyncThunk(
`${actionPrefix}/patch`,
async (args: actionPatchArgs thunkAPI) => {
try {
const { id, data, params } = args
const response = await service.patch(id, data, params)
const responseData = parseApiResponse(response)
return responseData
} catch (error) {
const feathersError: FeathersError = error
return thunkAPI.rejectWithValue(
_rejectValue(feathersError.toJSON())
)
}
},
{
additionalMeta: {
optimistic: true,
},
}
) With the |
Yeah, it's straightforward to add a static |
In some specific types of action we currently have a format where we include some meta information in the payload. A payload structure could look similar to { request: serialize(request), response: serialize(response), data: {...}}, this allows us to introduce a general middleware that can act on http related actions and pick up on general error cases like 5xx response code, either logging it or displaying some sort of notification. After looking into the RTK-Q I realize that the structure now will be 2 levels deep since the return value of transformResponse in RTK-query is stored in a data key. (A sidenote here is that it could be nice if it was spread out on the object?) To avoid that structure and because the serialized versions of the response and request object are just meta data about the action it would be nice if we from the payload generator could include some data in the meta structure. We also have a pattern where we use rejectWithValue with a similar structure. A api could be to just the check if there is a plain object returned from the payload generator of a object which defines both payload and meta keys. Those would then be reserved words. const action = createAsyncThunk(
"someaction/a/b/c",
async (args: actionPatchArgs thunkAPI) => {
if(random()){
// Object used as a payload directly. As previously
return {
a: 1
}
} else {
return {
payload: { a: 1},
// included in existing meta object
meta: { something: 1 }
}
}
},
) If you want a 100% backwards compatible solution you could also include a thunkApi.resolveWith(similar to rejectWith) that returns a special object that can be recognized and parsed. That way you would avoid conflicts with existing code which already has meta/payload keys in their returned payload. |
Hmm. Y'know, |
I created a outline suggestion, just for discussion (#1044). For rejected actions we could extend the rejectedWith callback with a optional second parameter which could be appended to the meta object. Then the signature of rejectWith and resolvedWith could be similar having the payload as the first argument and the optional extra meta data as the second. For pending actions I don't really have a very good suggestion. Currently you cannot customize the pending action (except for the idGenerator), so that will kind of not change. My use case(s) would not need to alter the pending actions, so for me that would be ok to leave the pending action as is. Alternatively as some other previously suggested in this thread we could have a static property, but I don't really see the benefit (if its static, well, then you shouldnt need to have it in the action?). With this suggestion the intended usage would be something like: const willBeResolved = createAsyncThunk(
"someaction/a/b/c",
async (args, thunkAPI) => {
const {
response,
request,
data // data is already parsed fromt he request object by doMyFetchQuery
} = await doMyFetchQuery();
return thunkApi.resovleWith(data, { request: serializeRequest(request), response: serializeResponse(response) });
});
const willBeRejected = createAsyncThunk(
"someaction/a/b/c",
async (args, thunkAPI) => {
const {
response,
request,
data // data is already parsed fromt he request object by doMyFetchQuery
} = await doMyFetchQuery();
return thunkApi.rejectWithValue(data, { request: serializeRequest(request), response: serializeResponse(response) });
})
// data passed as meta object data will be a part of the actions meta.extra object.
meta: {
arg,
requestId,
requestStatus: 'fulfilled' as const,
extra: result instanceof ResolveWithValue ? result.meta : null
} To be backwards compatible the meta argument to rejectWithValue (and resolveWithValue) is optional. If not passed the extra option in the meta object will be null. In my suggestion I chose to put the extra meta attributes into a (in lack of a better name) extra property on the meta object. This is to avoid potential naming collisions with the user provided meta data. If this is something you could be interested in I can work on a complete PR for a review. One problem I do see is that in order to get complete typescript coverage it would be needed to extend the types with even more parameters. So this will complicate the types even more. |
Resolved in #1083. |
Is it possible to type the custom meta? |
The context is code splitting and modules loaded dynamically on demand. We could say declare the slice, thunk!, and it's API in the store but implement it in dynamically loaded on demand module. |
@eolamisan I'm pretty sure that what was asked here is possible for years now. What feature is missing for you? |
It's possible to do this but I have not seen an example do this and I don't understand why this works.
|
i don't really understand what you're trying to do, but it's worth noting the first parameter you pass to createAsyncThunk is exposed as thunk.typePrefix - it's a prefix for the types for the action creators, not a type itself. you can also use the thunk.fulfilled action creator in an addCase, rather than constructing the string yourself. |
Why does this line wire the dynamically created thunk into the store?
|
I have never before seen create a reducer inside of a thunk - I have the gut feeling that you are trying to do something that could be done much simpler in a different way and are stuck in the middle of an XY problem. Could you maybe go a thousand steps back (far enough that the concepts "code" and "thunk" are not part of it) and try to explain what you want to do and why you want to do it? |
There are multiple dynamically on demand loaded modules. There is the feature X module. The X module depends on the store module and import API from store module. Implements X feature related logic declared in the store. There is the feature Y module. The Y module depends on the store module and import API from store module. Implements Y feature related logic declared in the store. The feature Y module want to dispatch an action.The logic for this action is in X module. (code splitting). The X related logic should stay in X module. Module Y can request the store to dispatch actions and doesn't need to know where they are implemented. Note (with reducers, thunks, actions): |
@eolamisan : I'm going to say that this seems like a discussion topic that is completely unrelated to this long-closed issue thread. If there are specific technical changes that you are asking for, could you open up a new issue thread to request those, and give details on what new APIs or changes you want to have and why? Otherwise, could you open up a new discussion thread to discuss how to use RTK in various use cases? Thanks! |
I have a couple of middlewares that get triggered by identifying the presence of few keys in the "meta" object on the action. "meta" of the action created by createAsyncThunk has few useful keys like "arg" & "requestId".
Is there any way to add custom key-values to the "meta" object along side these keys?
The text was updated successfully, but these errors were encountered: