5.0.17 • Published 4 years ago

redux-net2 v5.0.17

Weekly downloads
-
License
-
Repository
-
Last release
4 years ago

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:

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.