ngrx-requests
is a super simplified, user-friendly version of ngrx-query - the angular/ngrx version of redux-query
ngrx-requests
primary goal is to unopinionatedly destroy all the boilerplate that surrounds network-related redux state, namely the success, fail, error and data state entries, actions and reducer-related code.
By unopinionated, we mean users should be able to adopt ngrx-requests
pattern into their app a little at a time in a very user-friendly manner.
Stop the Boilerplate
Listening for request status - in progress, error, success - is something that's often encountered across redux apps. Does your store look like this?
feature: isFetching:true fetchError:... fetchSuccess:false data:nullfeature2: isFetching:false fetchError:null fetchSuccess:true data:......feature987: isFetching:false fetchError:null fetchSuccess:null data:null
Maybe your effects look like this?
Effect 1:@ myEffect$ = thisactions$ Effect 2:@ myEffect$ = thisactions$ ... Effect 987:@ myEffect$ = thisactions$
You should be letting the simple fact that you're making a request handle all this boiler plate for you.
ngrx-requests
uses an HttpInterceptor as a means to "listen in" on any request that you can match (and it provides some handy helper matchers, too!). When you register a listener, you'll be provided with Observables that give you that status of your request (IDLE, WORKING, SUCCESS, ERROR) and the request response. All of a sudden, your services can become the providers of all your data again, not your effects and not even the store, and all the boilerplate loading/success/error actions and repetitive cases in your reducers just disappear.
All you have to do, is make the request! No more dispatching success/fail actions, no more effects, no more selectors, no more boilerplate: just simply make the request.
Example:
; @ implements OnDestroy private static PATH = 'https://restcountries.eu/rest/v2/name/'; public status$: Observable<NgrxRequestStatus>; public meta$: Observable<any>; public request$: Observable<any>; private void; { status$: thisstatus$ meta$: thismeta$ dispose: thisdispose = thisrequests; } { thisrequest$ = thishttp; } { this; }
Your component
@ public NGRXSTATUS = NgrxRequestStatus; { } { return JSON; }
Template:
<!-- subscribe to the request --> Not doing anything Loading data... Failed Success! {{stringify((service.meta$ | async)?.body)}} {{stringify((service.meta$ | async))}}
(slightly more) Advanced:
You might not feel comfortable (I wouldn't blame you) to leave all of your data inside your NGRX_REQUESTS
slice of state in your store. Use an effect and it now becomes trivial to put your network-retrieved data wherever you want:
Add the request id to your service:
public ngrxRequestId: string; ... { id:thisngrxRequestId ... = thisrequests }
Have your effect listen for NgrxRequestAction.SUCCESS
:
{} @ myEffect$ = thisactions$ ;
Be unopinionated
This should be evident from the examples above. ngrx-requests
doesn't need you to create reducers, selectors, structure your store in a specific way, or require complex configuration, all you have to do to start using it.
Import it:
@
Start listening in on requests:
@ implments OnDestroy { thisdispose = thisrequestsdispose; } { this; }
API
ngrx-requests
is built with extreme simplicity, and thus flexibility, in mind. Here's what you need to know:
ngrxRequestsService
register(matcher: Matcher, transform?: Transformer): RequestData
matcher
- a function that accepts an HttpRequest and returns a boolean
transform
- optional - a function that can transform the success or error response from the async request.
If no transform
is provided, NgrxRequests uses it's default transformer which will update the status to NgrxRequestStatus.Success
if an HttpResponse
is received or NgrxRequestStatus.ERROR
if an HttpErrorResponse
is received.
If provided, NgrxRequests
will dispatch an NgrxRequestStatus.Success
action with any data that is returned from transform
of an NgrxRequestStatus.Error
action if an error is thrown.
Here's what the default transform
looks like to give you an idea:
const DEFAULT_TRANSFORM = { if r instanceof HttpErrorResponse throw r; return r;};
This is what it returns:
interface RequestData id: string; request$: Observable<NgrxRequestStatusObject>; meta$: Observable<any>; status$: Observable<RequestStatus>; isIdle$: Observable<boolean>; isCanceled$: Observable<boolean>; isError$: Observable<boolean>; isSuccess$: Observable<boolean>; isWorking$: Observable<boolean>;on a request or group of requests void;
id
- useful when in allowing you to listen to ngrx-request actions in your effects and dispatch your own actions.
dispose
- useful when you want to stop the (very little overhead) of having ngrx-requests listen to a request. When a service is destroyed, for instance, you'll probably want to call dispose()
in ngOnDestroy
.
The rest of the Observables are simply selectors to the NGRX_REQUESTS
slice of state for any request that the matcher you provided matches.
Matchers
ngrx-requests
helps you out by providing you HttpRequest
matchers to help get you started:
matchWithBody<T>(body:T)
matches any request where the body of the request matches the provided body with equality i.e. ===
.
// thismatchWithBody<string>'mybody'// will matchhttp;
matchWithBodyMatcher<T>(fn: (body: T) => boolean))
matches any request where the provided function that accepts the HttpRequest body returns true.
// this ));`// will matchhttp.post('/some-endpoint',{params:{key:'value'}});
matchWithHeader(name: string, val?: string)
matches any request where a header exists with the provided name and, if the optional val
is provided, the header matches it.
// this});// will matchhttp; // and this})// will matchhttp;
matchWithMethod(method: string)
matches any request where the method matches the provided method (case insensitive)
// this// will matchhttp;
matchWithUrl(url: string | RegExp)
matches any request whose url
matches the provided string or regular expression
// these ); // will matchhttpService;// but nothttpService;
matchWithParam(name: string, val?: string)
matches any request where a param exists with the provided name and, if the optional val
is provided, the param matches it.
// this});// will matchhttp; // and this});// will matchhttp;
And finally, the ultimate combinator helpers to make everything super-readable:
matchAll(...matchers: Matcher[])
:
Creates a matcher that returns true when all matchers passed in matches.
// this // will match const params = ; httpService;
matchAny(...matchers: Matcher[])
Creates a matcher that returns true when any matcher passed in matches.
// this // will match any of thesehttpService;httpService;httpService;
Finally, this is how you can use the matchers to help you:
thisngrxRequestsService;
If you've got a more specific use case, you can always provide your own custom matcher:
thisngrxRequestsService;
Actions
ngrx-requests
was inspired by this great article about why (and how) you can stop fetching data inside your ngrx effects. But that doesn't mean you should stop doing everything inside your effects. In fact, you're encouraged to use your effects to "listen" for NGRX_REQUESTS actions and map your fetched data into the appropriate part of your store using NgrxRequestAction.SUCCESS
If you aren't using a transform
, then each NgrxRequestSuccess
action will have a meta
property of the HttpResponse
:
@ myEffect$ = thisactions$ ;