redux-symbiote
TypeScript icon, indicating that this package has built-in type declarations

3.4.0 • Public • Published

redux-symbiote Build Status Coverage Status

All Contributors

Write your actions and reducers without pain

Usage

import { createSymbiote } from 'redux-symbiote'
 
 
const initialState = {
  error: null,
  accounts: [],
  loading: false,
}
 
const symbiotes = {
  accounts: {
    loading: {
      start: (state) => ({ ...state, loading: true }),
      failed: (state, error) => ({ ...state, loading: false, error }),
      finish: (state, accounts) => ({ ...state, loading: false, accounts }),
    },
  },
}
 
export const { actions, reducer } = createSymbiote(initialState, symbiotes)

Also you can use CommonJS:

const { createSymbiote } = require('redux-symbiote')
 
// ...

Demo

Edit Redux Symbiote Todos

API

Create symbiote

function createSymbiote(
  initialState,
  symbiotes,
  ?namespace = ''
)

Create action handlers + reducer

createSymbiote(initialState, {
  actionType: actionReducer,
  nestedType: {
    actionType: nestedActionReducer,
  }
})

Example:

const initialState = { value: 1, data: 'another' }
 
const symbiotes = {
  increment: (state) => ({ ...state, value: state.value + 1 }),
  decrement: (state) => ({ ...state, value: state.value - 1 }),
  setValue: (state, value) => ({ ...state, value }),
  setData: (state, data) => ({ ...state, data }),
  concatData: (state, data) => ({ ...state, data: data + state.data }),
}
 
export const { actions, reducer } = createSymbiote(initialState, symbiotes)
 
dispatch(actions.increment()) // { type: 'increment' }
dispatch(actions.setValue(4)) // { type: 'setValue', payload: [4] }
dispatch(actions.decrement()) // { type: 'decrement' }
dispatch(actions.setData('bar')) // { type: 'setData', payload: ['bar'] }
dispatch(actions.concatData('foo ')) // { type: 'concatData', payload: ['foo '] }
 
// State here { value: 3, data: 'foo bar' }

When you call actions.setValue symbiote calls your action handler with previousState and all arguments spreaded after state.

Nested example

const initialState = { value: 1, data: 'another' }
 
const symbiotes = {
  value: {
    increment: (state) => ({ ...state, value: state.value + 1 }),
    decrement: (state) => ({ ...state, value: state.value - 1 }),
  },
  data: {
    set: (state, data) => ({ ...state, data }),
    concat: (state, data) => ({ ...state, data: data + state.data }),
  },
}
 
export const { actions, reducer } = createSymbiote(initialState, symbiotes)
 
dispatch(actions.value.increment()) // { type: 'value/increment' }
dispatch(actions.value.decrement()) // { type: 'value/decrement' }
dispatch(actions.data.set('bar')) // { type: 'data/set', payload: ['bar'] }
dispatch(actions.data.concat('foo ')) // { type: 'data/concat', payload: ['foo '] }

Options

Third parameter in createSymbiote is optional string or object.

If string passed, symbiote converts it to { namespace: 'string' }.

Object has optional properties:

  • namespace is string — set prefix for each action type
  • defaultReducer is (previousState, action) -> newState — called instead of return previous state
  • separator is string — change separator of nested action types (default /)

ActionHandler##toString

You can use action as action type in classic reducer or in handleAction(s) in redux-actions

import { handleActions } from 'redux-actions'
import { createSymbiote } from 'redux-symbiote'
 
const initialState = { /* ... */ }
 
const symbiotes = {
  foo: {
    bar: {
      baz: (state, arg1, arg2) => ({ ...state, data: arg1, atad: arg2 }),
    },
  },
}
 
const { actions } = createSymbiote(initialState, symbiotes)
 
const reducer = handleActions({
  [actions.foo.bar.baz]: (state, { payload: [arg1, arg2] }) => ({
    ...state,
    data: arg1,
    atad: arg2,
  }),
}, initialState)

How to use reducer

createSymbiote returns object with actions and reducer.

Created reducer already handles created actions. You don't need to handle actions from symbiote.

// accounts.js
export const { actions, reducer } = createSymbiote(initialState, symbiotes, options)
 
// reducer.js
import { reducer as accounts } from '../accounts/symbiote'
// another imports
 
export const reducer = combineReducers({
  accounts,
  // another reducers
})

Why?

Redux recommends creating constants, action creators and reducers separately.

https://redux.js.org/basics/

const ACCOUNTS_LOADING_START = 'ACCOUNTS_LOADING_START'
const ACCOUNTS_LOADING_FAILED = 'ACCOUNTS_LOADING_FAILED'
const ACCOUNTS_LOADING_FINISH = 'ACCOUNTS_LOADING_FINISH'
 
 
export function loadingStart() {
  return {
    type: ACCOUNTS_LOADING_START,
  }
}
 
export function loadingFailed(error) {
  return {
    type: ACCOUNTS_LOADING_FAILED,
    payload: {
      error,
    },
  }
}
 
export function loadingFinish(accounts) {
  return {
    type: ACCOUNTS_LOADING_FINISH,
    payload: {
      accounts,
    },
  }
}
 
const initialState = {
  error: null,
  accounts: [],
  loading: false,
}
 
export function accountsReducer(state = initialState, action) {
  switch (action.type) {
    case ACCOUNTS_LOADING_START:
      return Object.assign({}, state, {
        loading: true,
      })
 
    case ACCOUNTS_LOADING_FAILED:
      return Object.assign({}, state, {
        loading: false,
        error: action.payload.error,
      })
 
    case ACCOUNTS_LOADING_FINISH:
      return Object.assign({}, state, {
        loading: false,
        accounts: action.payload.accounts,
      })
  }
 
  return state
}

So much boilerplate.

Let's look at redux-actions.

import { createActions, handleActions, combineActions } from 'redux-actions'
 
 
export const actions = createActions({
  accounts: {
    loading: {
      start: () => ({ loading: true }),
      failed: (error) => ({ loading: false, error }),
      finish: (accounts) => ({ loading: false, accounts }),
    },
  },
}).accounts
 
const initialState = {
  error: null,
  accounts: [],
  loading: false,
}
 
export const accountsReducer = handleActions({
  [combineActions(actions.loading.start, actions.loading.failed, actions.loading.finish)]:
    (state, { payload: { loading } }) => ({ ...state, loading }),
 
  [actions.loading.failed]: (state, { payload: { error } }) => ({ ...state, error }),
 
  [actions.loading.finish]: (state, { payload: { accounts } }) => ({ ...state, accounts }),
}, initialState)

But we have some duplicate in action creators properties and reducer.

Let's rewrite it to redux-symbiote:

import { createSymbiote } from 'redux-symbiote'
 
const initialState = {
  error: null,
  accounts: [],
  loading: false,
}
 
const symbiotes = {
  start: (state) => ({ ...state, loading: true }),
  finish: (state, { accounts }) => ({ ...state, loading: false, accounts }),
  failed: (state, { error }) => ({ ...state, loading: false, error }),
}
 
export const { actions, reducer: accountsReducer } =
  createSymbiote(initialState, symbiotes, 'accounts/loading')

That's all. accounts/loading is an optional namespace for actions types.

To reduce noise around loading actions try symbiote-fetching.

Contributors

Thanks goes to these wonderful people (emoji key):

Sergey Sova
Sergey Sova

📖 💻 💡 🤔 ⚠️
Arutyunyan Artyom
Arutyunyan Artyom

👀 🤔 🐛 💻
Igor Kamyshev
Igor Kamyshev

📦 ⚠️
Ilya
Ilya

🐛
Ivanov Vadim
Ivanov Vadim

📖
Аnton Krivokhizhin
Аnton Krivokhizhin

📦 🚇
Viacheslav
Viacheslav

🤔 👀
Dmitri Razin
Dmitri Razin

🐛 🎨
Surgie Finesse
Surgie Finesse

💻

This project follows the all-contributors specification. Contributions of any kind welcome!

Package Sidebar

Install

npm i redux-symbiote

Weekly Downloads

210

Version

3.4.0

License

MIT

Unpacked Size

33.7 kB

Total Files

11

Last publish

Collaborators

  • betula
  • sergeysova