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

v5.0.0 - Rewrite and simplify createAction API #143

Closed
piotrwitek opened this issue Apr 22, 2019 · 31 comments
Closed

v5.0.0 - Rewrite and simplify createAction API #143

piotrwitek opened this issue Apr 22, 2019 · 31 comments
Assignees
Milestone

Comments

@piotrwitek
Copy link
Owner

piotrwitek commented Apr 22, 2019

Issuehunt badges

Is your feature request related to a real problem or use-case?

Currently, createAction API is not optimal and require some unnecessary boilerplate code (historically it was created pre TS v3.0 so there was a lot of limitations having an impact on API shape):

const action = createAction('TYPE', resolve => {
    return (name: string, id: string) => resolve(id);
  });

Describe a solution including usage in code example

Today with recent TypeScript features I can finally rewrite the API to something simpler, more intuitive and more consistent across the board.

This will resolve few existing feature requests:

createAction

type User = { id: number, name: string };
const user: User = { id: 1, name: 'Piotr' };

const action1 = createAction('TYPE1')<string>();
action1(user.name); // => { type: 'TYPE1', payload: 'Piotr' }

const action2 = createAction(
  'TYPE1',
  (user: User) => user.name, // payload creator
)<string>();
action2(user); // => { type: 'TYPE1', payload: 'Piotr' }

const actionWithMeta1 = createAction('TYPE2')<string, number>();
actionWithMeta1(user.name, user.id); // => { type: 'TYPE2', payload: 'Piotr', meta: 1 }

const actionWithMeta2 = createAction(
  'TYPE2',
  (user: User) => user.name, // payload creator
  (user: User) => user.id, // optional meta creator
)<string, number>();
actionWithMeta2(user); // => { type: 'TYPE2', payload: 'Piotr', meta: 1 }

createAsyncAction

const action = createAsyncAction(
  'REQ_TYPE', 'SUCCESS_TYPE', 'FAILURE_TYPE', 'CANCEL_TYPE',
)<Request, Response, Error, undefined>();

const action = createAsyncAction(
  ['REQ_TYPE', (req: Request) => req], // request payload creator
  ['SUCCESS_TYPE', (res: Response) => res], // success payload creator
  ['FAILURE_TYPE', (err: Error) => err], // failure payload creator
  'CANCEL_TYPE', // optional cancel payload creator
)();

const actionWithMeta = createAsyncAction(
  'REQ_TYPE', 'SUCCESS_TYPE', 'FAILURE_TYPE', 'CANCEL_TYPE',
)<[Request, Meta], Response, [Error, Meta], undefined>();

const actionWithMeta = createAsyncAction(
  ['REQ_TYPE', (req: Request) => req, (req: Request) => 'meta'], // request payload & meta creators
  ['SUCCESS_TYPE', (res: Response) => res], // success payload creator
  ['FAILURE_TYPE', (err: Error) => err, (err: Error) => 'meta'], // failure payload & meta creators
  'CANCEL_TYPE', // optional cancel payload creator
)();

createCustomAction

All remaining non-standard use-cases you will be able to cover with createCustomAction

const action1 = createCustomAction('TYPE1', (name: string) => ({ payload: name }));

const action2 = createCustomAction('TYPE2', (name: string, id: string) => ({
  payload: name, meta: id,
}));

Who does this impact? Who is this for?

All TypeScript users

Additional context (optional)

createAction and createStandardAction will be deprecated because new createAction will be able to replace their function.

In v5, the old API will be kept in deprecated import so incremental migration is possible:

import { deprecated } from "typesafe-actions";

const { createAction, createStandardAction } = deprecated;

IssueHunt Summary

Backers (Total: $200.00)

Become a backer now!

Or submit a pull request to get the deposits!

Tips


IssueHunt has been backed by the following sponsors. Become a sponsor

@piotrwitek piotrwitek added this to the v5.0.0 milestone Apr 22, 2019
@piotrwitek piotrwitek self-assigned this Apr 22, 2019
@piotrwitek piotrwitek pinned this issue Apr 22, 2019
@piotrwitek piotrwitek changed the title Rewrite and simplify createAction API using generic tuple types v5.0.0 - Rewrite and simplify createAction API using generic tuple types Apr 22, 2019
@piotrwitek piotrwitek changed the title v5.0.0 - Rewrite and simplify createAction API using generic tuple types v5.0.0 - Rewrite and simplify createAction API Apr 23, 2019
@IssueHuntBot
Copy link

@IssueHunt has funded $150.00 to this issue.


@sjsyrek
Copy link
Contributor

sjsyrek commented Apr 26, 2019

This is not related to #116, right?

@piotrwitek
Copy link
Owner Author

createAction and createCustomAction will be deprecated.

@sjsyrek It means it would become unnecessary

It's still just a proposal, It's not finalized yet, open to discussion.

@piotrwitek
Copy link
Owner Author

I updated the proposal with improved createAsyncAction API that will resolve 2 existing feature requests:

@davidgovea
Copy link

davidgovea commented May 10, 2019

This looks great!

Edit - is there a way to set the error: boolean parameter of a FSA in this approach? These purpose-built payload and meta creation functions are more specific than the general "mapper" approach from before -- maybe I'm missing something

@piotrwitek
Copy link
Owner Author

Hey @davidgovea, please help me to figure out this feature.

I need to understand your use-case and how you would like to use this feature from a user perspective.
Could you send me a real usage example of how you're using the error flag with a current v4 API?
Please include actions, reducer and any other code that is invoking actions and setting error flag (I need to see what parameters you're passing to action creator with their types).

Thanks

@paolostyle
Copy link

paolostyle commented May 16, 2019

Would something like this work with the new API?

const action = createAsyncAction(
  'REQ_TYPE',
  ['SUCCESS_TYPE', res => {
    // do something with res
  }],
  'FAILURE_TYPE'
)<[Request, Meta], Response, [Error, Meta]>;

If not I guess I could simply pass identity function with the type in 1st and 3rd param, but something like this would also be pretty useful.

@piotrwitek
Copy link
Owner Author

piotrwitek commented May 16, 2019

Hey @paolostyle, that's an interesting idea, I think that might be possible. I haven't thought of it and I really appreciate your feedback.

I'll be working on the implementation this weekend so I'll give it a shot.

If that would work I think I would drop the second variant and use only the version from your proposal as it's basically solving both use-cases with one function which is a superior API.

Challenge 1:

One limitation is it'll only allow mapper functions with a single argument. I think it might still be ok because the previous version of async action has the same limitation and practically no one was complaining about it, so I guess most users can live without it.

Possible solution:

const action = createAsyncAction(
  'REQ_TYPE',
  ['SUCCESS_TYPE', (res, meta) => {
    // do something with res
  }],
  'FAILURE_TYPE'
)<[Request, Meta], (res: Response, meta: Meta) => Results, [Error, Meta]>;

@bboydflo
Copy link

bboydflo commented May 30, 2019

will v5 release fix createAction to handle optional payload/meta? I think currently it doesn't.

// this keeps the correct type
export const decrement1 = (payload?: number) => action(DECREMENT, payload)

// while this doesn't
export const decrement2 = createAction(DECREMENT, action => {
  return (payload?: number) => action(payload);
});

@piotrwitek
Copy link
Owner Author

@bboydflo yes you are correct, createAction is currently not supporting optional payload type correctly, it'll be fixed in new API.

@jaridmargolin
Copy link

Hi @piotrwitek - These changes look great. I wanted to quickly check-in and see how progress was coming.

Thank you for all the hard work :)

@piotrwitek
Copy link
Owner Author

Just returned from a Summer Break, will come back to this real soon!

@jstheoriginal
Copy link

Not sure if this is isolated to 5.0.0-3 or not (if you suspect it's not just let me know and I'll make a new issue), but createReducer allows you to add extra items than what's on the defined state. It properly type-checks keys defined in the state type for the reducer...it just allows extra keys (not the end of the world, unless someone makes a spelling mistake).

Example (the extra property has no typescript errors reported..doing anything invalid to the other two properties properly shows errors):

export type AppState = {
  previous: AppStateStatus | null;
  current: AppStateStatus;
};

export type AppStateReducerState = DeepReadonly<{
  appState: AppState;
}>;

export const defaultAppReducerState: AppStateReducerState = {
  appState: {
    previous: null,
    current: 'active',
  },
};

export const appStateReducer = {
  appState: createReducer(defaultAppReducerState.appState).handleAction(
    setAppState,
    (state, { payload }) => ({
      previous: state.current,
      current: payload,
      extra: 'why is this allowed?',
    }),
  ),
};

@piotrwitek
Copy link
Owner Author

@jstheoriginal It's a good find, thanks for reporting.
It would be great to have it as a separate issue to make sure it's a known problem.

@mAAdhaTTah
Copy link
Contributor

@jstheoriginal Not specific to 5.0. I remember this coming up when I attempted to use createReducer once before. Did you open an issue? If not, I can.

@jonrimmer
Copy link

Hey, thanks for the great library! It makes working with using react-redux with TS a lot better. I was wondering if you'd seen the TypeScript action creator code in NgRx, the Angular redux library? It does a good job of avoiding boilerplate, while retaining type safety for things like reducers. Might be a source of inspiration.

@issuehunt-oss
Copy link

issuehunt-oss bot commented Oct 17, 2019

@jonrimmer has funded $50.00 to this issue.


@issuehunt-oss issuehunt-oss bot added the 💵 Funded on Issuehunt This issue has been funded on Issuehunt label Oct 17, 2019
@piotrwitek
Copy link
Owner Author

piotrwitek commented Oct 17, 2019

Hey @jonrimmer,
Thanks for adding to the funding, that's really appreciated!
Regarding your suggestion, I haven't seen it, could you link to some more advanced usage examples/scenarios that are including payloads, meta, async actions so I could compare with new API proposal and check for possible improvements? Thanks!

@jonrimmer
Copy link

No problem! The actual creators are fairly simple, they work like your examples above. The main thing I noticed is that they've managed to get type inference on the reducer functions without having to register root actions or extend the module, e.g.

const scoreboardReducer = createReducer(
  initialState,
  on(loadTodos, state => ({ ...state, todos })),
  on(ScoreboardPageActions.awayScore, state => ({ ...state, away: state.away + 1 })),
  on(ScoreboardPageActions.resetScore, state => ({ home: 0, away: 0 })),
  on(ScoreboardPageActions.setScores, (state, { game }) => ({ home: game.home, away: game.away }))
);

So, similar to createReducer() and handleAction() in the current version of typesafe-actions, just slightly smoother. Not sure how they do it though!

@piotrwitek
Copy link
Owner Author

piotrwitek commented Oct 17, 2019

@jonrimmer actually that automatic inference is easy to add, what differentiate typesafe-actions from all other libraries is the fact that createReducer knows about all valid actions in the system (that's why you register RooAction) and informs you in real-time which actions are already handled in this reducer and which not allowing for quicker understanding of the bigger picture without a need to browse through the files containing declarations of actions. I can guess this is most certainly not the case in the above example because it is impossible without extra type parameter providing the constraint type.

@piotrwitek
Copy link
Owner Author

@Sir-hennihau
Copy link

Sir-hennihau commented Dec 11, 2019

For anyone coming from search engines with the following bug:

argument 1 is invalid it should be an action-creator instance from typesafe-actions

My mistake was that I didn't see the docs were not updated yet. I solved it by using the new createAction() API given by @piotrwitek in the first post of this thread.

Example:

export const updateProviders = createAction(
  "UPDATE_PROVIDERS",
  (provider: Provider) => provider,
)<Provider>();

Go and checkout the other breaking changes https://github.com/piotrwitek/typesafe-actions/releases which are not in the official docs yet, too.

@mAAdhaTTah
Copy link
Contributor

Maybe this should be a new issue, but I'm not sure I understand the benefit/point of the extra function call in createAction. Given the above example:

export const updateProviders = createAction(
  "UPDATE_PROVIDERS",
  (provider: Provider) => provider,
)<Provider>();

The generic Provider is... provided... twice. Does this actually help the typing? I'm curious why this is necessary.

@piotrwitek
Copy link
Owner Author

piotrwitek commented Dec 24, 2019

@mAAdhaTTah good question.
Looking at the above example you don't want to use extra function call in that case, because it's not designed for it. There is a simpler way to do that:

export const updateProviders = createAction(
  "UPDATE_PROVIDERS",
)<Provider>();

But it can be useful in other more complex cases, especially when you want to set the constraints for the types you expect.

Consider this:

const deleteUser = createAction(
  'TYPE1',
  (user: User) => user.id,
)<string>();

deleteUser(userObject) // { type: 'TYPE1', payload: string }

This will create an action creator that will accept argument user of type User, but the payload will be string.
Hope that is clear!

@DalderupMaurice
Copy link

DalderupMaurice commented Mar 4, 2020

Is there a possibility to add meta data to all the actions?
With this snippet:

const actionWithMeta = createAsyncAction(
  'REQ_TYPE', 'SUCCESS_TYPE', 'FAILURE_TYPE', 'CANCEL_TYPE',
)<[Request, Meta], Response, [Error, Meta], undefined>();

I can call actionWithMeta.success(request, myMeta); and the meta is applied, but it gives a typing error, expecting only 1 argument.

I'd need to have a certain metadata field on ALL the actions for my custom redux middleware.

@piotrwitek
Copy link
Owner Author

@DalderupMaurice yes you can just edit the types arguments:

const actionWithMeta = createAsyncAction(
  'REQ_TYPE', 'SUCCESS_TYPE', 'FAILURE_TYPE', 'CANCEL_TYPE',
)<[Request, Meta], [Response, Meta], [Error, Meta], [undefined, Meta]>();

@elegos
Copy link

elegos commented Mar 29, 2020

Hello! I'm trying to configure a basic action/reduce setup. I've got the following action:

export const editorMetaAuthorChange = createAction('editor/META_AUTHOR_CHANGE')<string>()

Then this should be the relative reducer:

export interface EditorState {
  meta: {
    author: string
    name: string
    description: string
    tags: string[]
  }
}

const initialState: EditorState = {
  meta: {
    author: '',
    name: '',
    description: '',
    tags: [],
  },
}

const reducer = createReducer(initialState)
  .handleAction(editorMetaAuthorChange, (state, { payload }) => ({
    ...state,
    meta: {
      ...state.meta,
      author: payload,
    },
  }))

The problem is that in handleAction if I don't specify the state's and the action's types, they will be any. What am I doing wrong? Shouldn't typesafe-actions take care of this?

Thanks :)

@allandiego
Copy link

allandiego commented May 5, 2020

Hello! I'm trying to configure a basic action/reduce setup. I've got the following action:

export const editorMetaAuthorChange = createAction('editor/META_AUTHOR_CHANGE')<string>()

Then this should be the relative reducer:

export interface EditorState {
  meta: {
    author: string
    name: string
    description: string
    tags: string[]
  }
}

const initialState: EditorState = {
  meta: {
    author: '',
    name: '',
    description: '',
    tags: [],
  },
}

const reducer = createReducer(initialState)
  .handleAction(editorMetaAuthorChange, (state, { payload }) => ({
    ...state,
    meta: {
      ...state.meta,
      author: payload,
    },
  }))

The problem is that in handleAction if I don't specify the state's and the action's types, they will be any. What am I doing wrong? Shouldn't typesafe-actions take care of this?

Thanks :)

Im having same issue

// actions
export const updatePreferenceRequest = createAction(
  '@preference/UPDATE_PREFERENCE_REQUEST'
)<UpdatePreferenceData>();

// REDUCER
const INITIAL_STATE: StatePreference = {
  totalCountMax: 100,
  vibration: true,
  sound: true,
   loading: false,
};

const preferenceReducer = createReducer(INITIAL_STATE)
.handleAction(
  updatePreferenceRequest,
  (state: StatePreference, { payload }) => ({
    ...state,
    loading: true,
  });

state and payload
Parameter 'state' implicitly has an 'any' type

@elegos
Copy link

elegos commented May 5, 2020

@allandiego I've kind of "resolved" writing a supporting library: #229

@allandiego
Copy link

@elegos i ended up switching to the official redux lib:
https://redux-toolkit.js.org

@viters
Copy link

viters commented May 21, 2020

@piotrwitek

Are generics not supported for createCustomAction createHandler?

enum Element {
  Elem1 = 'elem1',
  Elem2 = 'elem2',
}

type ElementValueMapper = {
  [Element.Elem1]: string;
  [Element.Elem2]: number;
};

type Explicit = ElementValueMapper[Element.Elem1]; // correct type string

const action = createCustomAction(
  'someAction',
  <T extends Element>(e: T, value: ElementValueMapper[T]) => ({
    payload: { e, value },
  })
);

action(Element.Elem1, 5); // type of value is string | number, should be only string

It is possible when I create action by hand:

const action = <T extends Element>(
  name: T,
  value: ElementValueMapper[T]
): PayloadAction<'changevalue', { name: T; value: ElementValueMapper[T] }> =>
  ({
    type: 'changevalue',
    payload: { name, value },
  } as const);

action(Element.Elem1, 5); // Argument of type '5' is not assignable to parameter of type 'string'.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests