-
-
Notifications
You must be signed in to change notification settings - Fork 2k
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
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
RFC: Add new Higher Order Effect - createApiEffect #1826
Comments
createApiEffect
I am not sure if it should be about effects or actions but there is not only API calls that use this pattern of I would suggest something like // create
const callApiActions = createAsyncActions('...', requestProps, successProps, failureProps);
// consume
const callApiRequestAction = callApiActions.request;
const callApiSuccessAction = callApiActions.success;
const callApiFailureAction = callApiActions.failure; Of course, the creation of this kind of actions (meaning action types) must follow a common pattern like appending the word |
This would be cool. We have a helper in our codebase that does something very similar. Some suggestions from our implementation:
|
@jonrimmer I like the concept of a |
What about array deconstruction? I thought of that now and it can be an interesting idea. And from my previous post, here is what it can look like: // create & ready to consume
const [
callApiRequestAction,
callApiSuccessAction,
callApiFailureAction
] = createAsyncActions('callApi', {
successProps: props<{ response: string[] }>(),
failureProps: props<{ error: any }>()
});
// unionize
union({
callApiRequestAction,
callApiSuccessAction,
callApiFailureAction
}); With new functions that have this definition: const createAsyncActions = createAsyncActionsFactory({
updateRequestActionType?, // identity function by default
updateSuccessActionType?, // suffix 'fulfilled' at the end of every action?
updateFailureActionType? // suffix 'failed' at the end of every action?
}); // which returns a 'createAsyncActions' function generator
createAsyncActions(baseActionType: string, {
requestProps?,
successProps?,
failureProps?
}); // which returns all 3 actions with array deconstruction |
I thought about reducing code of async effect today. Here is my idea, following my previous post. Imagine you already applied the createAsyncActions(baseActionType: string); // no props at all, seems unprobable but why not
createAsyncActions<TRequestProps>(baseActionType: string); // no props for success and failure
createAsyncActions<TRequestProps, TSuccessProps>(baseActionType: string); // no props for failure
createAsyncActions<TRequestProps, TSuccessProps, TFailureProps>(baseActionType: string); Of course, we can still provide the definition with a config object as second parameter. And then, here we simplify effects: Define actions// create & ready to consume
const callApiActions = createAsyncActions<{}, { result: string[] }, { error: any }>('callApi');
const [
callApiRequestAction,
callApiSuccessAction,
callApiFailureAction
] = callApiActions;
// unionize
union({
callApiRequestAction,
callApiSuccessAction,
callApiFailureAction
}); Define effectsNow, we can use the callApi$ = createAsyncEffect(
callApiActions,
callApiObservable,
'merge'
); And its type definition would be: createAsyncEffect(
callApiActions: AsyncActions, // the array of 3 actions
callApiObservable: Observable<TResult>, // TResult should match the result type of the success action
operation: 'merge' | 'concat' | 'switch' | 'concat'
); The actions are used to filter the request action and then dispatch the result (success, failure). The observable should return the result of the async effect, it can be an API call, a retrieve/save to local-storage or any kind of observable-like operation. And finally, the operation will be used to define the rxjs operator to use on top of the observable (mergeMap, concatMap, switchMap or concatMap). |
I think that this use case can be more generic (as mentioned earlier in this thread). Instead of handling async actions/effects start, success or failure, it would be useful to have a well defined action life-cycle which NGRX tracks (e.g. dispatched, error, cancel etc.) . Then you could ask NGRX what state a certain action has and use that where needed. I am aware of effects life-cycle but there is no implicit distinction between success and failure. |
I think "lifecycle" is a great name for what we're talking about here, a high-level pattern for capturing a particular, common flow found across many uses of NgRx, that looks something like this: I really like @Odonno's suggestion of having helper functions to create all the lifecycle actions in one go, and being able to pass them them into the effect creator. Or maybe there could be a single creator function that produces both the effect and the actions: const { actions, effect } = createLifecycle(task, 'callApi'); |
I was thinking of taking this even a bit further where the life-cycle is “baked” into NGRX itself and the library manages that out of the box. You can just query the state (e.g. via an operator). |
Yeah, that makes sense as well, so you could also do: const { selectors, reducer } = createLifecycle(task, 'callApi'); And the reducer state would be something like: interface LifecycleState<TResult, TError> {
state: null | 'waiting' | 'running';
result: TResult | null;
error: TError | null;
lastStatus: 'success' | 'cancel' | 'error' | null;
} And Although I think you'd still want to make the underlying actions and effect available, in case people wanted to customise the reducer logic, or use them to trigger additional effects. |
@jonrimmer Oh my. I never thought we could go one step further. Your example of However, I do not really understand how you will use a
But is that a replacement of PS: And really nice schema BTW |
@Odonno I don't see the reducer/selectors as replacing the effect so much as automatically reflecting its opaque, internal state in a way that you can bind to within the UI. E.g if you have an effect, but don't have a reducer/state, how would you know when to show the loading component? |
Well. Yes, you have to incorporate it in the root state. But, now I get it. The |
Does this align more with the goals of |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
Scenario
Most apps that use NgRx for API requests involve effects that handle a
request
,success
andfailure
action.request
effect listens for arequest
ofType and then makes an API call that returns anObservable
.Observable
successfully emits asuccess
action is returned with theObservable
value as the payload.Observable
throws an error afailure
action is returned with the error object as the payload.Example using
createEffect
Proposed Solution
Example using new
createApiEffect
With this being a common pattern it might be nice to create a new type of effect that handles this scenario in a predictable way. Since
createEffect
is just a function, we can use composition to wrapcreateEffect
with a new function namedcreateApiEffect
.Let's revisit the above example using a newly imagined higher-order effect creator named
createApiEffect
. Our new effect would look like the following:This provides a much cleaner visual to developers, avoids nesting, and helps prevent developers from placing operators in the wrong areas, e.g.
catchError
in the wrong place.To accomplish this we will make use of the new
mapToAction
operator in #1822. By default,createApiEffect
would useconcatMap
but we can pass throughoperator
as well.Potential Implementation of
createApiEffect
A first pass of
createApiEffect
that makes use ofmapToAction
would look something like this:Other information:
In addition to adding
createApiEffect
as a new type of effect creator, I would recommend and will assist with writing a quickguide
on how to construct your own higher-order effect creators.Solution above depends on #1822 for
mapToAction
.If accepted, I would be willing to submit a PR for this feature
[x] Yes (Assistance is provided if you need help submitting a pull request)
[ ] No
The text was updated successfully, but these errors were encountered: