Skip to content

Jan-Jan/redux-observable-sans-boilerplate

Repository files navigation

redux-observable-sans-boilerplate

Redux is awesome. But, boilerplate is a code smell. This library allows you to use redux with zero boilerplate:

  • ZERO constant definitions
  • ZERO action definitions
  • ONLY 1 dispatch definition (per http API targeted)

(and similar for websockets). Instead of boilerplate, you should just be coding business logic.

install

npm i --save redux-observable-sans-boilerplate

use

See example directory for a simple example of how to use library

http (json)

import { httpEpic, httpAction } from 'redux-observable-sans-boilerplate'

Setting up the middleware as per https://redux-observboilerplateable.js.org/docs/basics/SettingUpTheMiddleware.html

For our example we will be doing http request to two different backends.

const optionsGH = {
    url: 'https://api.github.com', // site that will be 
    id = 'GH', // optional value used to differentiate http action types
}

const optionsCustom = {
    url: 'https://mysite.url',
    // optionally use setHeaders to do authentication
    setHeaders: (state$, deps) => ({  Authorization: `Bearer ${action$.value.me.token}` }),
}

const rootEpic = combineReducers(httpEpic(optionsGH),httpEpic(optionsCustom))

The action creators have the signature httpAction(dispatch, id)(type, url, method = 'GET', settings = {})(body) where:

  • dispatch as provided by reaca-redux
  • id is optional and should be the same value as used to create the httpEpic
  • type is the front part of the type of action you will be dispatching, eg, 'GET_USERS':
    • httpEpic will turn the dispacted action into GET_USERS_REQUEST, GET_USERS_SUCCESS and GET_USERS_FAILURE type actions
    • the _FAILURE will be followed by either a HTTP_ERROR or HTTP_UNAUTHORIZED type action
      • if id is defined, eg, GH, then the following types would be changed to HTTP_GH_ERROR or HTTP_GH_UNAUTHORIZED
    • a GET_USERS_CANCELLED type action can be used to cancel the ongoing request
  • url is the latter part of the url path; it will be appended to front piece defined in the corresponding httpEpic
  • method can be GET, POST, PATCH, etc.
  • settings is the standard options that can be passed to fetch
  • body is optional; it is the JSON object, eg, from you form

To use you all you need to do is use connect(mapStateToProps, mapDispatchToProps) from react-redux where

const mapDispatchToProps = dispatch => ({
    http: httpAction(dispatch, id), // same id as in corresponding httpEpic
})

which you can call from anywhere in you app, eg,

const mapDispatchToProps = (dispatch, ownProps) => ({
    onSubmit: ownProps.http('GET_USERS', 'users'),
})

where your data is then passed into onSubmit(data).

websocket: command and query (and "event")

I created a command and query for websockets for convention sake, but it works perfectly fine if you just use command.

import { socketEpics, commandAction, toAction } from 'redux-observable-sans-boilerplate'

Setting up the middleware as per https://redux-observboilerplateable.js.org/docs/basics/SettingUpTheMiddleware.html

You need to have defined a socket object

const auth = (state$, deps) =>
    ({ token: state$.value.me.token, userId: state$.value.me.userId })

const rootEpic = combineReducers(...socketEpics(socket))

Note: The auth function is completely optional.

To use you all you need to do is use connect(mapStateToProps, mapDispatchToProps) from react-redux where

const mapDispatchToProps = dispatch => ({
    command: commandAction(dispatch),
})

which you can call from anywhere in you app, eg,

const mapDispatchToProps = (dispatch, ownProps) => ({
    onSubmit: ownProps.command('updateUser'),
})

where your data is then passed into onSubmit(data).

Bonus: to handle events from backend you can use

socket.on(`event`, toAction(dispatch))

it expects the data object received to include a type which will then be converted constant-case, and then dispatched as an action.

If a CommandRejected or QueryRejected type is received a further action of _FAILURE type will also be emitted (based on the constant-case of the payload.type -- idealy that would be the type of the corresponding command or query).