Redux-toolkit: createAction doesn't support a `meta` attribute

Created on 21 Jun 2019  路  29Comments  路  Source: reduxjs/redux-toolkit

And as far as I can tell there is no way to possibly insert it.

not a big deal, any action that has meta attributes, you can just manually create.

If I were to make a proposal, it would be that it would be a second argument.

All 29 comments

Yep, and it doesn't support customizing payload either, other than passing it in directly. This is something I'd like to handle in some way before calling this 1.0. See #82 .

The two main questions are how createAction() should accept callbacks for payload and meta customization, and how createSlice() should accept them.

The obvious inspirations here would be:

https://redux-actions.js.org/api/createaction#createactiontype-payloadcreator-metacreator

https://github.com/ericelliott/autodux#action-creators

Based on that, I'm thinking roughly:

type PayloadCreator = (...args) => Payload;
type MetaCreator = (...args) => Meta;

createAction(
    type : String,
    payloadCreator? : PayloadCreator ,
    metaCreator? : MetaCreator ,
)

createSlice(
    reducers: {
        simpleReducer: ReducerFunction,
        complexReducer: {
             payload? : PayloadCreator ,
             meta? : MetaCreator ,
             reducer : ReducerFunction
        } 
    }
)

Actual TS types to be figured out, of course, but you get the idea.

I'm just exploring options right now. Would something like

type PrepareAction<OriginalPayload, Payload, Meta> = (originalPayload: OriginalPayload) => {payload: Payload; meta? : Meta}

createAction(
    type : String,
    prepare?: PrepareAction,
)

createSlice(
    reducers: {
        simpleReducer: ReducerFunction,
        complexReducer: {
             prepareAction: PrepareAction,
             reducer : ReducerFunction
        } 
    }
)

also be acceptable? That could be way easier to type.

How do we currently workaround the inability to customise the payload with arguments? This seems like an extremely crucial missing feature. Almost every reducer I have needs the payload to be adjusted based on the function parameters.

For example, a reducer that opens an error message snackbar and displays a message. The message can't be hardcoded into the action creator as obviously the error message changes based on what the actual error is.

Or a very common case - saving form input values to state.

@TidyIQ : yes, I'm well aware of this issue :) There's an open PR (#149 ) to try to implement that capability, but we're trying to figure out what the best API would look like.

Fixed by #149 and out as 0.6.3 . Please let me know if that works for you!

I've seen how "prepare" works but i'm trying to add parameters to "meta" without having to use "payload", i'm doing this to make it work:

const teamsSlice = createSlice({
  slice: 'teams',
  initialState,
  reducers: {
    loadTeams: {
      reducer: (state: TeamsState) => {
         state.loading = true
      },
      prepare: (payload?: any) => ({
        payload,
        meta: {
          request: {
            endpoint: '/teams'
          }
        }
      })
    }
  }
});

I want to create that action:

{
  type: 'teams/loadTeams',
  meta: {
    request: {
      endpoint: '/teams'
    }
  }
}

It's possible to do this in createSlice() without having to use payload?.

@JonatanPineda : not quite sure what you're asking here.

You don't have to declare any arguments for the prepare callback, and you don't have to return a payload field if you don't want to. However, for whatever object you return, _only_ the payload and meta fields will be included in the final action object.

@markerikson I'm just looking to create an action with the meta property using createSlice() style, i guess is like that:

Captura de Pantalla 2019-08-19 a la(s) 10 03 01

but an error is generated:

Captura de Pantalla 2019-08-19 a la(s) 10 14 27

so when i add the payload argument all works:

Captura de Pantalla 2019-08-19 a la(s) 10 18 47

but the final object that i want to create doesn't have payload, i only want to produce this:

{
  type: 'teams/loadTeams',
  meta: {
    request: {
      endpoint: '/teams'
    }
  }
}

how to achieve this correctly?

It may be because you're not declaring a type for the action object in the reducer. Then again, it might be that we need to improve our TS types somehow.

I'm sorry I didn't show all the error:

Type '{ reducer: (state: TeamsState) => void; prepare: () => { meta: { request: { endpoint: string; }; }; }; }' is not assignable to type 'CaseReducer | EnhancedCaseReducer'.
Type '{ reducer: (state: TeamsState) => void; prepare: () => { meta: { request: { endpoint: string; }; }; }; }' is not assignable to type 'EnhancedCaseReducer'.
Types of property 'prepare' are incompatible.
Type '() => { meta: { request: { endpoint: string; }; }; }' is not assignable to type 'PrepareAction'.
Type '() => { meta: { request: { endpoint: string; }; }; }' is not assignable to type '(...args: any[]) => { payload: any; }'.
Property 'payload' is missing in type '{ meta: { request: { endpoint: string; }; }; }' but required in type '{ payload: any; }'.ts(2322)
createAction.d.ts(16, 5): 'payload' is declared here.

I typed the action with 'AnyAction' in the reducer and the error persists:

Captura de Pantalla 2019-08-19 a la(s) 11 06 17

Probably the project needs to enable that feature, I think it's necessary because of the simplicity of createSlice() we can reduce a lot of redux-typescript stuff and mixed with the ability to add meta properties to use middleware's like redux-offline, it would be such a huge improvement.

Okay, I just added #176 to track this.

Stick with that workaround for now. We'll try to get the TS types updated in the near future to allow returning just {meta} by itself.

Note that you should be able to at least skip having payload as an argument, and just return {payload: undefined, meta: whateverValueHere}.

@JonatanPineda : after thinking about it further, I've closed #176 . I feel that the payload: undefined workaround is sufficient for now, especially given how complicated the createAction types are already and that almost all use cases would involve passing an actual payload value anyway.

However, for whatever object you return, _only_ the payload and meta fields will be included in the final action object.

I'm wondering why adding that restriction and not simply just send the object that the prepare function returned with all of it's properties (meta, error, etc.).

This is kinda contradictory to this part of the documentation:

Additionally, the object can have a meta and/or an error field that will also be added to created actions. meta may contain extra information about the action, error may contain details about the action failure. These three fields (payload, meta and error) adhere to the specification of Flux Standard Actions.
Here

@0w3w : We added support for the error field after this issue, but those are still the only fields that are accepted when returned from the prepare callback.

The point is that the FSA spec only allows {type, payload?, error?, meta?}, so we ignore anything else.

I agree that only FSA properties should be allowed, then, Is there any reason why those properties are not available on the case reducers? I think they should. Accessing error and meta information on the reducer is something I've seen on several projects and is very useful when using FSA.

E.G.

export const doSomethingSlice = createSlice({
    name: 'dosomething',
    initialState: { foo: "bar"},
    reducers: {
        DoSomething: {
            reducer: (state, action) => {
                console.log(action.meta); // <- meta/error is not accesible
            },
            prepare: () => ({ payload: undefined, meta: "something" })
        }
    }
});

Am I missing something? Also, Kudos for the super quick response!

Those properties _will_ be accessible, so I'm not sure why you're saying they're not. That's one of the reasons why we made sure to continue passing the entire action object as an argument to reducers, when some other implementations of a createReducer utility only pass the payload field itself. Do you have any kind of a sandbox or project that looks like those fields _aren't_ available?

@0w3w You always need to type action, even with a prepare method present. As it stands, we can check that you are not using a different action than what you return from prepare, but not automatically infer that type.

import {createSlice, PayloadAction} from '@reduxjs/toolkit'

export const doSomethingSlice = createSlice({
  name: 'dosomething',
  initialState: { foo: "bar"},
  reducers: {
      DoSomething: {
          reducer: (state, action: PayloadAction<undefined /*payload*/, string /*type*/, string/*meta*/>) => {
              console.log(action.meta);
          },
          prepare: () => ({ payload: undefined, meta: "something" })
      }
  }
});

@phryneas Bravo! That is what I was missing!
Thanks you both for your quick turnaround times!

@phryneas : hmm, not sure I was aware of that. I don't think we've got it covered in the "Usage with TS" page.

@phryneas : hmm, not sure I was aware of that. I don't think we've got it covered in the "Usage with TS" page.

Good point. There's no createSlice with prepare example in there. We should add that.

Ah, I see why looking at the type-tests. There I'm only testing the payload and that is just typed exactly as the case without prepare so I didn't bother mentioning it.

My assumption was that the reducers only access the payload as I personally only ever used the other FSA properties from middlewares, never from a reducer. But I guess that's fair game, so definitely needs to go into the docs (and another type test I guess)

Hey, so I stepped into another TS error with this:
Screenshot from 2020-01-16 12-20-55

With error:

Type '{ reducer: (state: { foo: string; }, action: PayloadAction<undefined, string, string, never>) => void; prepare: () => { payload: undefined; meta: string; }; }' is not assignable to type 'CaseReducer<{ foo: string; }, { payload: any; type: string; }> | CaseReducerWithPrepare<{ foo: string; }, { payload: any; type: string; }>'.
  Type '{ reducer: (state: { foo: string; }, action: PayloadAction<undefined, string, string, never>) => void; prepare: () => { payload: undefined; meta: string; }; }' is not assignable to type 'CaseReducerWithPrepare<{ foo: string; }, { payload: any; type: string; }>'.
    Types of property 'reducer' are incompatible.
      Type '(state: { foo: string; }, action: PayloadAction<undefined, string, string, never>) => void' is not assignable to type 'CaseReducer<{ foo: string; }, { payload: any; type: string; }>'.
        Types of parameters 'action' and 'action' are incompatible.
          Type '{ payload: any; type: string; }' is not assignable to type 'PayloadAction<undefined, string, string, never>'.
            Property 'meta' is missing in type '{ payload: any; type: string; }' but required in type '{ meta: string; }'.ts(2322)
typings.d.ts(555, 5): 'meta' is declared here.

I think the problem is that the SliceCaseReducer type ... | CaseReducerWithPrepare<State, PayloadAction<any> only accepts ANY.

I forked and tried to fix this by adding additional supported types:

export type SliceCaseReducers<State> = {
  [K: string]:
    | CaseReducer<State, PayloadAction<any>>
    | CaseReducerWithPrepare<State, PayloadAction<any>>
    | CaseReducerWithPrepare<State, PayloadAction<any, string>>
    | CaseReducerWithPrepare<State, PayloadAction<any, string, any>>
    | CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>
}

But, while my changes fix the issue, it breaks types.test.ts on inferred types:

    Semantic: /home/gmolina/github/redux-toolkit/type-tests/files/createSlice.typetest.ts (73, 19) Parameter 's' implicitly has an 'any' type.

    Semantic: /home/gmolina/github/redux-toolkit/type-tests/files/createSlice.typetest.ts (73, 24) Binding element 'payload' implicitly has an 'any' type.

    Semantic: /home/gmolina/github/redux-toolkit/type-tests/files/createSlice.typetest.ts (147, 19) Parameter 'state' implicitly has an 'any' type.

    Semantic: /home/gmolina/github/redux-toolkit/type-tests/files/createSlice.typetest.ts (155, 19) Parameter 'state' implicitly has an 'any' type.

    Semantic: /home/gmolina/github/redux-toolkit/type-tests/files/createSlice.typetest.ts (190, 17) Parameter 'state' implicitly has an 'any' type.

    Semantic: /home/gmolina/github/redux-toolkit/type-tests/files/createSlice.typetest.ts (261, 17) Parameter 'state' implicitly has an 'any' type.

Here is my proposed change with updated documentation and a test for it.

I didn't create PR cause the tests are not passing. Any guidance on this?

This doesn't error for me in this sandbox: https://codesandbox.io/s/brave-sky-ulhsx
Or am I doing something differently from you?

Could you create me a reproducible? I'd like to see the actual error before discussing a possible solution ;)

I'm using strict type checking, changed tsconfig.json to:

{
  "compilerOptions": {
    "module": "commonjs",
    "jsx": "preserve",
    "esModuleInterop": true,
    "sourceMap": true,
    "allowJs": true,
    "lib": [
      "es6",
      "dom"
    ],
    "rootDir": "src",
    "moduleResolution": "node",
    "strict": true,
  }
}

Then the error shows. The project is configured with strict type-checking https://github.com/reduxjs/redux-toolkit/blob/master/tsconfig.json so I think this should work?

As a temporary workaround disabling strictFunctionTypes on the tsconfig fix this error.

Sorry, I somehow didn't see your message earlier.

The thing is, the example I linked to you has strict: true - this is the tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "jsx": "preserve",
    "esModuleInterop": true,
    "sourceMap": true,
    "allowJs": true,
    "strict": true,
    "lib": [
      "es6",
      "dom"
    ],
    "rootDir": "src",
    "moduleResolution": "node"
  }
}

So there's gotta be some other difference?

Here's a code sandbox with a reproducible error:
https://codesandbox.io/s/reduxjs-toolkit-issue-148-nsh03

error

I just need some guidance and I may be able to fix it on my branch and create a PR ;)

Also this seems like a new error for this thread, should I create a new Issue?

@0w3w sorry, I didn't get to this earlier as I was sick. I took a stab at it and opened #350 . Could you please install the CI build and give it a try if it fixes your problems?

yarn add https://pkg.csb.dev/reduxjs/redux-toolkit/commit/9023d835/@reduxjs/toolkit

Was this page helpful?
0 / 5 - 0 ratings