Redux-toolkit: Setting "meta" on the action generated by "createAsyncThunk"

Created on 3 Oct 2020  Â·  30Comments  Â·  Source: reduxjs/redux-toolkit

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?

Most helpful comment

Yep, passing type was exactly the kind of thing I was figuring.

Perhaps something like...

type MetaCallback = (type: string, originalMeta: ThunkMeta, thunkApi: ThunkApi) => infer extraMetaProperties or something?

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?

All 30 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 action.meta.arg.

What specifically are you needing to add to the meta field for those actions?

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 .rejected - it would be nice to capture some additional information about the (in our case) axios request without having to wrap in a try / catch (for instance, request.url , etc.)
In our app, we're capturing the existing xx_FAILURE actions to send to our error logging service via custom middleware - I thought I could just replace that with action.type.endsWith('/rejected') butmetaanderror` don't provide the same information

The keys need not end up in fulfilled, pending, and rejected actions. Because i don't want to throttle or debounce fulfilled action or other actions. I want to control the api hit happening inside of payloadCreator. I get that, that is the reason for existence of condition option.

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
};

```ts
// 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 `true` or `false`.

It would be easy to implement debouncing using a middleware.

Here is the middleware implementation,
```ts
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 createAsyncThunk.

Being able to set keys on meta field, might not solve the problem. Because action returned by the createAsyncThunk is not a action object that can be controlled by the middleware above, but is a thunk action. Debounce functionality has to be implemented internally and made available in the options object of createAsyncThunk.

Thank you, will appreciate your thoughts on it @markerikson

FWIW, I can see some potential use cases for being able to define meta for createAsyncThunk-generated actions. I'm open to ideas on what the API changes might look like. Perhaps an additional callback field in the options object?

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 notifications@github.com 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 meta option in the options object of the createAsyncThunk. This meta options could be just an object that takes any key-values or could be a callback just in case if the meta is derived, as you said @markerikson .

But should this meta end up in fulfilled, pending, rejected actions?? For my case, the meta has to end up on thunk.

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 thunk, meta object can be placed as a property of thunk function and then returned. This would enable middlewares that looks for them.

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?

@sanathsharma :

For my case, the meta has to end up on thunk

I'm sorry, that statement doesn't make sense. Thunks are functions. They don't have a type, payload, or meta. Only action objects do.

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 thunk middleware. Order of the middlewares matter though.

As I said,

Being able to set keys on meta field, might not solve the problem. Because action returned by the createAsyncThunk is not a action object that can be controlled by the middleware above, but is a thunk action.

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 thunkActionCreator.meta to actually do or cause to happen? I've never seen anyone suggest doing that before.

As of now, it might be specific to this case.
With that I can control whether the thunkActionCreator gets forwarded to the thunk middleware or not, with a custom middleware before that.

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 abort method.

Coming to meta field on other actions, If callback has to be called 3 times, each for specific action, then maybe type has to included in the argument for each call, so that different objects can be returned based on that if needed.

Yep, passing type was exactly the kind of thing I was figuring.

Perhaps something like...

type MetaCallback = (type: string, originalMeta: ThunkMeta, thunkApi: ThunkApi) => infer extraMetaProperties or something?

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 type, arg, requestId, getState, extra is all the information that is needed.

Lemme phrase it another way: what would you expect thunkActionCreator.meta to actually do or cause to happen? I've never seen anyone suggest doing that before.

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.

@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:

  • is _not_ an action object
  • does _not_ have a type field
  • does _not_ have payload, meta, or error fields

It's a function. It will be intercepted by the thunk middleware immediately, and not passed any further.

For that matter, fetchUser, the _thunk action creator_, won't even get passed to dispatch at all.

So, I was responding to this comment:

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 thunk middleware. Order of the middlewares matter though.

As I said,

Being able to set keys on meta field, might not solve the problem. Because action returned by the createAsyncThunk is not a action object that can be controlled by the middleware above, but is a thunk action.

But I realised, the fact that its a function does not restrict us from having these properties on them.

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 additionalMeta field to the options and insert it as a field to the meta object.

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 meta dynamically.

Like, it's straightforward enough to add support for an optional callback that accepts, say, (actionType, requestId, thunkArg) or something, and spread the returned fields into action.meta. That can also easily be used for "static" values, like {timeout: 1234}.

But, it sounds like there's really a desire for dynamically generating some fields for meta, like:

I was just looking for something similar, but not at the point of dispatch, but rather in .rejected - it would be nice to capture some additional information about the (in our case) axios request without having to wrap in a try / catch (for instance, request.url , etc.)

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 getState() or state to these callbacks. That would almost definitely make the types too tricky.

So, is there anything people are wanting to see besides (actionType, requestId, thunkArg) ?

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)
I use it in my project in combination with an adapted version of redux-optimistic-ui the following way:

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 redux-optimistic-ui library changes immediately affect the redux store. In case of a server error the changes are reverted.

Yeah, it's straightforward to add a static additionalMeta property. My assumption here is that people want to add _dynamic_ meta values based on properties of the request or response itself, and that's where it gets complicated. So, I'm asking for more details on what potential inputs people would want to have available to make those calculations.

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, thunkApi.resolveWith might actually be the nicest API option here, and potentially easier to deal with than adding an options.metaCallback param or something. On the other hand, because the payload creation callback runs _after_ the pending action is dispatched, this wouldn't allow you to add any meta field to the pending action - just fulfilled. (and for that matter, what about rejected ?)

I created a outline suggestion, just for discussion (https://github.com/reduxjs/redux-toolkit/pull/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.

Was this page helpful?
0 / 5 - 0 ratings