redux-symbiote
v3.4.0
Published
Write your actions and reducers without pain
Downloads
907
Readme
redux-symbiote
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
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
isstring
— set prefix for each action typedefaultReducer
is(previousState, action) -> newState
— called instead of return previous stateseparator
isstring
— 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):
This project follows the all-contributors specification. Contributions of any kind welcome!