Skip to content
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

Closed
sanathsharma opened this issue Oct 3, 2020 · 40 comments
Closed

Setting "meta" on the action generated by "createAsyncThunk" #750

sanathsharma opened this issue Oct 3, 2020 · 40 comments

Comments

@sanathsharma
Copy link

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?

@markerikson
Copy link
Collaborator

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?

@sanathsharma
Copy link
Author

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.

@jstclair
Copy link

jstclair commented Oct 4, 2020

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') but metaanderror` don't provide the same information

@sanathsharma
Copy link
Author

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

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.

@sanathsharma
Copy link
Author

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

@markerikson
Copy link
Collaborator

markerikson commented Oct 4, 2020

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?

@jstclair
Copy link

jstclair commented Oct 5, 2020

That sounds promising - you were thinking specifically for generating meta, or getting passed existing the existing meta (so you could add custom properties?)

@markerikson
Copy link
Collaborator

Well, that's kind of the question - I'm asking what your ideas are for the API and what specific needs you have :)

@jstclair
Copy link

jstclair commented Oct 5, 2020 via email

@sanathsharma
Copy link
Author

sanathsharma commented Oct 5, 2020

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.

@sanathsharma sanathsharma reopened this Oct 5, 2020
@markerikson
Copy link
Collaborator

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())
}, [])

@sanathsharma
Copy link
Author

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.

@markerikson
Copy link
Collaborator

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.

@sanathsharma
Copy link
Author

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.

@markerikson
Copy link
Collaborator

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?

@sanathsharma
Copy link
Author

Yes, that's great. May be type, arg, requestId, getState, extra is all the information that is needed.

@joseph1125
Copy link

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

@markerikson
Copy link
Collaborator

@joseph1125 but that would be about accessing data in the action, not data attached to the thunk action creator. Totally different things.

@joseph1125
Copy link

@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

@markerikson
Copy link
Collaborator

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.

@stanislav-halyn
Copy link

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.

@markerikson
Copy link
Collaborator

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) ?

@markerikson
Copy link
Collaborator

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.

@markerikson
Copy link
Collaborator

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.

@markerikson markerikson removed this from the 1.6 milestone Apr 26, 2021
@pineappledafruitdude
Copy link

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.

@markerikson
Copy link
Collaborator

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.

@trmjoa
Copy link

trmjoa commented May 7, 2021

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.

@markerikson
Copy link
Collaborator

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 ?)

@trmjoa
Copy link

trmjoa commented May 10, 2021

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.

@markerikson
Copy link
Collaborator

Resolved in #1083.

@edrpls
Copy link

edrpls commented Jul 30, 2021

Is it possible to type the custom meta?

@phryneas
Copy link
Member

phryneas commented Jul 30, 2021

@edrpls Yes. https://redux-toolkit.js.org/usage/usage-with-typescript#createasyncthunk

@eolamisan
Copy link

eolamisan commented May 29, 2024

The context is code splitting and modules loaded dynamically on demand.
I'd like to declare the thunk API in the store in a slice with createasyncthunk without providing implementation.
And do code splitting and implement the slice and reducers in a module loaded dynamically on demand.
Usind replareReducer, combineReducers and the reducerManager example.
The current workaround is hardcoded strings for reducer names.

We could say declare the slice, thunk!, and it's API in the store but implement it in dynamically loaded on demand module.

@phryneas
Copy link
Member

@eolamisan I'm pretty sure that what was asked here is possible for years now. What feature is missing for you?

@eolamisan
Copy link

It's possible to do this but I have not seen an example do this and I don't understand why this works.

import {declaredThunk, declaredThunkName} from 'store'
...
// this does not work
// declaredThunk.type is not possible
const theThunk = createAsyncThunk(declaredThunk.type, async () => {
      await ApiCall();
    });
    store.injectReducer(
      'slice-name',
      createReducer({}, builder => {
        builder.addCase(declaredThunk.fulfilled);
      })
    );
    
    // instead hardcoded strings have to be used
    // it's also not clear why this works
    const theThunk = createAsyncThunk(declaredThunkName, async () => {
      await ApiCall();
    });
    store.injectReducer(
      'slice-name',
      createReducer({}, builder => {
        builder.addCase(`${declaredThunkName}/fullfulled`);
      })
    );
    ```

@EskiMojo14
Copy link
Collaborator

EskiMojo14 commented May 29, 2024

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.

@eolamisan
Copy link

Why does this line wire the dynamically created thunk into the store?

builder.addCase(theThunk.fulfilled);

@phryneas
Copy link
Member

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?

@eolamisan
Copy link

There are multiple dynamically on demand loaded modules.
There is the globals store module.
The store declares and exports all the available API signatures to dependent 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).
Module doesn't need to know what module imlements the logic.

The X related logic should stay in X module.
The Y related logic should stay in Y module.
The modules X and Y should not create a circular dependency.

Module Y can request the store to dispatch actions and doesn't need to know where they are implemented.

Note (with reducers, thunks, actions):
Here the store would facilitate IOC and act similar to dependency injection container.
The store module declares the API: actions, reducers, thunks signatures. But they are implemented in the respective feature module.

@markerikson
Copy link
Collaborator

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

@reduxjs reduxjs locked as resolved and limited conversation to collaborators May 29, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests