Skip to content

ericdouglas/onli-reducer

Repository files navigation

onli-reducer

onli -> one line reducer

Codeship Status for ericdouglas/onli-reducer Coverage Status npm version

NPM

onli-reducer is a micro-library that helps you to write applications using redux/useReducer without boilerplate.

Works with whatever state management that gives you a dispatch method and expects you to call such method passing an object to it with a type property.

Table of Contents

Benefits over traditional redux/useReducer usage (with switch)

  • No boilerplate code.
  • Literally one line reducer (no need to use switch statement).
  • Direct call to actions without instructions about their type.
  • Support for asynchronous calls (dispatch/send actions from async functions).
  • No need to manually write your types.

Install

npm install --save onli-reducer

Basic Concepts

onli-reducer expose 4 methods in order to help you eliminate boilerplate code from your reducers, dispatch calls and application in general.

Although we expose 4 methods, you will normally just use the onli and onliSend method.

Reducer Solution

Instead of create a reducer function with an evergrowing switch statement inside of it, you just need to:

  1. Declare your actions as plain JavaScript functions;
  2. Attach such functions to an actions object;
  3. Pass the actions object to onli() helper.

The onli helper method returns an array with 2 elements: a reducer function and an array of strings (types), that is generated based on your actions' names.

Example:

import onli from "onli-reducer"

const increment = state => state + 1
const decrement = state => state - 1

const actions = { increment, decrement }
const [countReducer, types] = onli(actions)

export { countReducer, types }

Dispatch Solution

Istead of manually set the type in every dispatch call, you can just:

  1. Pass your dispatch method (from Redux or useReducer) and types array (from onli) for onliSend.
  2. Call your actions directly.

Important:

All your actions will receive in the action object dispatched at least two properties: type and send. You can pass any additional payload to your actions.

The type property is a string to let your reducer know which function it has to invoke.

The send property is an object that holds all other public actions so you can pass it to async functions in order to update your state after async calls.

You can see how it is used in a real app here.


Example:

import { onliSend } from "onli-reducer"
import { countReducer, types } from "./count.reducer"

// ...

const send = onliSend(dispatch, types)
const { increment, decrement } = send

// You can also:
// const { increment, decrement } = onliSend(dispatch, types)

// ...

<Counter
  value={count}
  onIncrement={() => increment()}
  onDecrement={() => decrement()}
/>

Usage / Examples

Using onli-reducer with React Hooks + Context

See a full example with asynchronous calls so you can have a glimpse of how onli-reducer would perform in real-world applications.

In such example app you will see:

  • data fetching
  • loading state transition
  • React hooks useState, useReducer and useContext
  • React Context API
  • asynchronous actions called from your reducer
  • more...

See the live demo here and the source code here.

Using onli-reducer with Redux

To show a comparison between the traditional usage of Redux and the new approach using onli-reducer, let's rebuild the Counter app from Redux docs.

Obs: it will be shown only the differences between the two approaches.

index.js with Redux:

// ...
import counter from "./reducers"

const store = createStore(counter)

// ...

<Counter
  value={store.getState()}
  onIncrement={() => store.dispatch({ type: "INCREMENT" })}
  onDecrement={() => store.dispatch({ type: "DECREMENT" })}
/>

index.js with Redux + onli-reducer:

// ...
import { createStore } from "redux"
import { onliSend } from "onli-reducer"
import { countReducer, types } from "./reducers"

const store = createStore(countReducer, 0)
const send = onliSend(store.dispatch, types)
const { increment, decrement } = send

// ...

<Counter
  value={store.getState()}
  onIncrement={() => increment()}
  onDecrement={() => decrement()}
/>

reducers/index.js with Redux:

export default (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1
    case "DECREMENT":
      return state - 1
    default:
      return state
  }
}

reducers/index.js with Redux + onli-reducer:

import onli from "onli-reducer"

const increment = state => state + 1
const decrement = state => state - 1

const actions = { increment, decrement }
const [countReducer, types] = onli(actions)

export { countReducer, types }

You can see/edit the example above here:

Edit counter

OBS: although in this simple example it can not be so evident the benefits of using onli-reducer, for real-world applications the amount of boilerplate code that onli-reducer helps you to not type is considerable.

Conventions

Inside your.reducer.js file, attach your synchronous functions to the actions object and name your asynchronous functions with an underscore, so you know they are both private and async.

These async functions will be triggered from your sync ones, and after finish their job such async functions will be able to dispatch sync functions to update the state.

It is a good practice to keep your reducer "pure", only dealing with sync functions.

You can see how it is implemented in our example app.

API

onli(actions)

The onli method expects an object that contains your public/synchrounous actions. It returns an array with two elements: a reducer (function) and an array with strings that represents your types ([string]).

import onli from "onli-reducer"

const increment = state => state + 1
const decrement = state => state - 1

const actions = { increment, decrement }

const [countReducer, types] = onli(actions)

export { countReducer, types }

onliSend(dispatch, types)

The onliSend method receives a dispatch function and a types array of strings.

It will return an object with methods attached to it. You will use these methods to dispatch actions in order to update your state.

///// Without onli-reducer
dispatch({ type: "increment" })
dispatch({ type: "increment", step: 5 })

///// With onli-reducer
increment()
increment({ step: 5 })

onliSend also adds to your action payload an object called send, that holds itself, the object returned from calling onliSend(dispatch, types) that have access to all your actions.

This is very useful because with such object you can pass it for async functions so them can dispatch actions to update your state after finish their async tasks.

// async/private action from your reducer
const _getPokemon = async ({ name, send }) => {
  const { showLoading, hideLoading, updateStore } = send // <- access to your sync methods

  showLoading()

  try {
    const { data } = await axios.get(`${URL}${name}`)
    updateStore({ pokemon: data }) // <- update your state after success
    hideLoading()
  } catch (error) {
    updateStore({ warning: "Ops... Pokémon not found" }) // <- update your state after failure
    hideLoading()
  }
}

onliReducer(actions)

Receives an actions object and return a reducer.

onliTypes(actions)

Receives an actions object and return an array with strings (types).

License

MIT License © Eric Douglas