redux-net2 v5.0.17
Cancellable, intercept-able HTTP request handling for redux using actions and reducers.
Features
- One, central reducer for HTTP requests, so no need to write request reducers in many cases
- Sending, cancelling and invalidating requests via dispatching actions
- Elegant and concise way for writing request action creators
- Intercepting request and response parameters
- Ability to use various request handlers under the hood
- Responses and errors are actions too
- Ability to pass context to request and receive it in response and error results
Installation
npm install --save redux-net
Getting started
Setup for handling JSON requests looks like this:
import {
createFetchHandler,
createHttpRequestHandlerMiddleware,
HttpRequestInterceptor,
HttpResponseInterceptor
HttpReducer,
} from 'redux-net'
import { combineReducers } from 'redux'
// Let's add common parameters to all dispatched requests, such as
// host, port, etc.
const RequestInterceptor: HttpRequestInterceptor = params => ({
scheme: 'http',
host: 'localhost',
port: 8000,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
...params, // .. but give an ability to developers to override them.
})
// Intercept all successful responses and decode the received JSON string
const ResponseInterceptor: HttpResponseInterceptor = response => ({
...response,
body: JSON.parse(response.body),
})
// Bundle the request handler and interceptors into a middleware
const HttpRequestHandlerMiddleware = createHttpRequestHandlerMiddleware(
createFetchHandler(fetch),
RequestInterceptor,
ResponseInterceptor,
)
const RootReducer = combineReducers({
http: HttpReducer, // Or ImmutableHttpReducer, that uses immutable.js data structures under the hood.
})
// After that, just add the middleware to applyMiddleware arguments list.
Defining request action creators
Request action creators are defined with $request function. It accepts request name And a request constructor function as shown below:
import { $request } from 'redux-net'
const fetchTodos = $request('TODO/ALL_FETCH', () => ({
path: '/todos',
})
const fetchTodo = $request('TODO/FETCH', (id: number) => ({
key: id, // If not specified, the first parameter value will be used as a fallback (if it is string or number).
path: '/todos',
})
// You may also pass request name and request payload creator as a record:
const login = $request({
name: 'USER_LOGIN',
payload: (username: string, password: string) => ({
method: 'post',
path: '/todos',
body: {
username,
password,
},
})
})
Request name identifies a kind of request, so it should be unique per request-kind (e.g. post creation or comment deletion).
Request payload is a plain object returned from request constructor.
You can see detailed API of the request and response payload objects later. For now, we used path and key. key is an optional property, that, if specified, identifies the request itself (e.g. post by id #4 or user by username "example"). If key is not specified, first argument of a request constructor will be used as a fallback key. If neither a request constructor accept at least one parameter, nor a key is specified - each new request of a kind will overwrite the previous state of the exact same kind, but not other requests - as long as request name uniqueness is reserved.
Example usage
import { cancel, invalidateResponse, invalidateCache } from 'redux-net'
import { fetchTodos, fetchTodo } from './actions'
import { store } from './store'
// Cancel last fetchTodo request:
store.dispatch(fetchTodos())
store.dispatch(cancel(fetchTodos))
// Cancel any fetchTodo request:
store.dispatch(fetchTodo(1))
store.dispatch(fetchTodo(2))
store.dispatch(cancel(fetchTodos, 1)) // Only first request would be cancelled.
// Invalidate last response of fetchTodo request:
store.dispatch(fetchTodos())
store.dispatch(invalidateResponse(fetchTodos))
// Invalidate any response of fetchTodo request:
store.dispatch(fetchTodo(1))
store.dispatch(fetchTodo(2))
store.dispatch(invalidateResponse(fetchTodos, 1))
// Invalidate all responses of fetchTodo requests:
store.dispatch(fetchTodo(1))
store.dispatch(fetchTodo(2))
store.dispatch(invalidateCache(fetchTodos))
Defining request state selectors
Request state selectors are defined with the createRequestReducer function. It accepts a request action creator, defined by the $request function, as the only argument and returns a selector function. The selector function itself accepts a state and an optional key parameter. If key is specified, the returned value will be the state of a request identified by the given key. If key is not specified, the state of the last request will be returned. The returned request state has the following shape:
{
pending: boolean // Whether or not the request is pending.
request: HttpRequest | null // The request payload or null.
data: any // The response body or null
error: any // The request error or null
token: any // The response token, which is unique for each completed request,
// either successfull or failed.
}
Additionally, redux-net ships with utility functions to further select from the request state:
import { createRequestReducer, isInitialized, isPending, isFailed, data, error, token } from 'redux-net'
import { fetchTodos, fetchTodo } from './actions'
import { store } from './store'
const todos = createRequestReducer(fetchTodos)
const todo = createRequestReducer(fetchTodo)
const state = store.getState()
// Whether or not the last fetchTodos request is initialized.
isInitialized(todo(state))
// Whether or not any fetchTodos request is initialized.
isInitialized(todo(state, 2))
// Whether or not the last fetchTodos request is pending.
isInitialized(todo(state))
// Whether or not any fetchTodos request is pending.
isInitialized(todo(state, 1))
// Whether or not the last fetchTodos is failed.
isFailed(todo(state))
// Response body of the last fetchTodos request or null.
data(todo(state))
// Error from the last fetchTodos request or null.
data(todo(state))
// Token of the last fetchTodos request.
data(todo(state))
If you want to manually access request name you could use someRequest.requestName.
API
request payload object accepts the following properties:
- headers - (optional) An object with request header names as keys and header values as values.
- method - (optional) HTTP method, default is get.
- scheme - (optional) Protocol, either https or http.
- username - (optional) Username for HTTP basic authorization.
- password - (optional) Password for HTTP basic authorization.
- host - (optional) Host, either domain name (e.g. example.com) or IP address (e.g. 127.0.0.1).
- port - (optional) Port number.
- path - (optional) Request path (e.g. /todos).
- query - Query object with query parameter names as keys and query parameter values as values. A value can be one of the following: - string - That is the most common case, e.g. { search: 'today' } is translated to: ?search=today - array - E.g. { hours: '9AM', '13PM' } is translated to: ?hours0=9AM&hours1=13PM - object - E.g. { contact: { username: 'XXXXXX', phone: 'XXX-XX-XX-XX' } } is translated to: ?contactusername=XXXXXX&contactphone=XXX-XX-XX-XX. Note that any level of nesting is supported (E.g. objects can contain objects with arrays, strings and objects with strings).
- body - (optional) Request body.
- context - (optional) A context object that will be contained in the response or error action's payload (whichever is dispatched).
- cache - (optional) See: https://developer.mozilla.org/en-US/docs/Web/API/Request/cache
- credentials - (optional) See: https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
- integrity - (optional) See: https://developer.mozilla.org/en-US/docs/Web/API/Request/integrity
- keepalive - (optional) See: https://developer.mozilla.org/en-US/docs/Web/API/Request/keepalive
- mode - (optional) See: https://developer.mozilla.org/en-US/docs/Web/API/Request/mode
- redirect - (optional) See: https://developer.mozilla.org/en-US/docs/Web/API/Request/redirect
- referrer - (optional) See: https://developer.mozilla.org/en-US/docs/Web/API/Request/referrer
- referrerPolicy - (optional) See: https://developer.mozilla.org/en-US/docs/Web/API/Request/referrerPolicy
response action's payload holds an object with the following properties:
- headers - Holds an object with response header names as keys and header values as values.
- status - Holds status code of the response (e.g. 200).
- statusText - Holds status text respectively to status of the response (e.g. 200).
- body - Holds body of the response. The raw data-type of the body is string, but the actual data-type depends on the response interceptor.
- context - (optional) A context object passed to the corresponding request or an empty object.
Writing interceptors
An interceptor is a simple function taking a thing of some type and returning a new thing of the exact same type (but with different value(s), most likely inheriting from the original thing).
- Request interceptor is a function, that accepts a request object and returns a new request object, but maybe with some changes, e.g. path, headers, e.t.c.
- Response interceptor is a function, that accepts a response object and returns a new response object, but maybe with some changes, e.g. encoded and even flattened body.
Implementing custom request handler
In most of the cases, the createFetchHandler will satisfy your needs, but if it doesn't, you can implement your own handler. Request handler is a bridge between redux-net and some low-level API that actually does the dirty job of sending requests and receiving responses. Because of it's flexible and cancellable nature, redux-net request handlers are implemented using RxJs under the hood. Further reading the docs, I assume that you have an intermediate knowledge of the mentioned technology and Functional Reactive Programming (FRP).
Request handler is a function that takes request parameters object as the only argument and returns an Observable of response object. Let's start writing our new request handler, that will use XhrRequest under the hood. First we need to import the required functions from redux-net and RxJs library, that we will use later:
import { generateUrl } from 'redux-net'
import { Observable } from 'rxjs'
Than, we need to write the function in it's simplest form:
const XhrHandler = (request) => new Observable(observer => {
//
})
So far, our handler creates just a never ending stream. Next step is to add basic request handling to our new handler.
const XhrHandler = (request) => new Observable(observer => {
const xhr = new XmlHttpRequest()
const requestMethod = request.method ? request.method.toUpperCase() : 'GET'
const url = generateUrl(request)
xhr.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
const response = {
headers: {},
status: this.status,
statusText: this.statusText,
body: xhr.responseText,
context: request.context || {},
}
observer.next(response)
observer.complete()
}
}
xhr.open(requestMethod, url)
xhr.send()
return () => {
xhr.abort()
}
})
What we do here is the following:
- We create a new instance of XmlHttpRequest()
- Retrieve method from the request object making it upper case and if it is not present there, fall back to 'GET'
- We use special utility function generateUrl, that ships with redux-net library, to generate url from the request object.
- Next, we register a listener that listens to the created requests ready state changes, and when it is in its ready, (e.g. readyState is 4 and status is 200) we retrieve the response as text and construct the response object in the form, that we are supposed to output to. After this, we state that stream is completed by calling the observer.complete() method.
- Next, we open the request with the detected method and generated url and send it.
- Finally we return a function that will be called for clean-up and there we call the xhr.abort(), so that, if the request is cancelled before the request completes, we are in no more need to wait to it.
Note, that for simplicity we ignored request and response headers and other parameters, error handling, etc. tThe above example is for demonstration purposes, actual production-ready implementation would be much more complex.
4 years ago