v5.0.0 - Rewrite and simplify `createAction` API
piotrwitek opened this issue ยท 31 comments
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
- Checkout the Issuehunt explorer to discover more funded issues.
- Need some help from other developers? Add your repositories on IssueHunt to raise funds.
IssueHunt has been backed by the following sponsors. Become a sponsor
@IssueHunt has funded $150.00 to this issue.
- Submit pull request via IssueHunt to receive this reward.
- Want to contribute? Chip in to this issue via IssueHunt.
- Checkout the IssueHunt Issue Explorer to see more funded issues.
- Need help from developers? Add your repository on IssueHunt to raise funds.
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.
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
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
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.
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]>;
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);
});
@bboydflo yes you are correct, createAction is currently not supporting optional payload type correctly, it'll be fixed in new API.
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 :)
Just returned from a Summer Break, will come back to this real soon!
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?',
}),
),
};
@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.
@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.
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.
@jonrimmer has funded $50.00 to this issue.
- Submit pull request via IssueHunt to receive this reward.
- Want to contribute? Chip in to this issue via IssueHunt.
- Checkout the IssueHunt Issue Explorer to see more funded issues.
- Need help from developers? Add your repository on IssueHunt to raise funds.
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!
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!
@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.
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.
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.
@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!
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.
@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]>();
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 :)
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 beany
. 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
@allandiego I've kind of "resolved" writing a supporting library: #229
@elegos i ended up switching to the official redux lib:
https://redux-toolkit.js.org
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'.