A tiny library (~1k gzipped) to use redux-style reducers locally in your React components. No external dependency, it can be used with or without a redux store.
npm install --save react-local-reducer
# or
yarn add react-local-reducer
For using hooks, see react-hooked-reducer.
As you scale your Redux application, your store will grow. Over time, as you add more and more reducers to the store, performance will decrease and bundle size will increase.
With react-local-reducer
, you can split your store into smaller chunks, and isolate their performance impact. Each chunk is located within the section of your app which depends on it - perfect for code-splitting.
One root cause of some of the problems associated with a large Redux store is that reducers need to be imported at store creation. This means they cannot be code-split in vanilla Redux, and while there are solutions which allow you to add or remove reducers dynamically, these solutions have trade-offs.
Scaling a redux application means adding more and more reducers to your global store, most of which will end up being irrelevant to the components which are mounted at any given time. Reducers are a bit like singletons, and complexity is added when they relate to a specific component (which isn't a singleton):
- You end up having to manage initialisation or reset actions
- Having multiple instances of a same component adds the overhead add and remove actions, forces you to identify the origin of an action and to add more logic in your reducer.
Another issue is that all reducers are called with every action dispatched to the store. If you have a huge number of reducers, this means each action will trigger a huge number of reducers - even the ones which are irrelevant to that action.
Not everything needs to be in a global store, especially state which is not shared across your application, and which strongly relates to a single component. When a reducer is coupled to a component, it can consume global state and global actions, but its state and actions no longer weigh down the store. Emerging architectures like Apollo Client consist of a redux store to cache network data (entities), with the rest of your state managed locally.
- This library offers a similar solution in React alone: local state which colocates reducers with their components
- Reducers are created using
props
andcontext
so they can be initialised, and they must return an object - Reducers can receive actions from your main store, with the help of a store enhancer provided with this package
withReducer
is a higher-order component which adds a reducer to a component. Its API is similar to connect
, with mapStateToProps
being replaced by a reducer. By default, it will spread the output of its reducer to props, alongside action creators bound to your Redux store. Selectors are not necessary.
import React from 'react'
import { withReducer } from 'react-local-reducer'
// Reducer
const reducerFactory = ({ initialCount }) =>
(state = { count: initialCount }, action) => {
if (!action) return state
if (action.type === 'ADD') {
return ({
count: state.count + 1
})
}
if (action.type === 'REMOVE') {
return ({
count: state.count - 1
})
}
return state
}
// Action creators
const add = () => ({ type: 'ADD' })
const remove = () => ({ type: 'REMOVE' })
// Component
const Counter = ({ count, add, remove }) => (
<div>
Count: { count }
<button onClick={ add }>Plus</button>
<button onClick={ remove }>Remove</button>
</div>
)
export default withReducer(
reducerFactory,
{ add, remove }
)(Counter)
The above created component would be used as follow:
<Counter initialCount={ 10 } />
One benefit of this approach is that you can have as many counters as you want, without adding complexity to your store.
You can customise how props
, state
and actionCreators
are passed through to your base component by adding a mergeProps
function.
This function has the signature (props, state, actionCreators) => ({ /* ... */ })
, and the returned object will be passed to the base component as props.
For example, if you wish to pass your state
as a single object:
(props, state, actionCreators) => ({ ...props, state, ...actionCreators })
Or if you wish for the separate props to be available in addition to being combined as a single object:
(props, state, actionCreators) => ({ ...props, state, ...state, ...actionCreators })
By default, props
, state
and actionCreators
are simply merged together.
You can pass your store
and other dependencies using setContextType(context: ReactContext)
or the last argument of withReducer
.
import { setContextType } from 'react-local-reducer'
setContextType(DependencyContext)
Don't forget to set its value with a its provider. When using withReducer
, you need to define a reducer creator (factory).
const reducerCreator = (props, context) => { /* return reducer */ }
You can have your local reducers receive any action your global redux store receives. Simply use the redux store enhancer onDispatchStoreEnhancer
.
import { createStore } from 'redux'
import { onDispatchStoreEnhancer } from 'react-local-reducer'
const store = createStore(reducer, initialState, onDispatchStoreEnhancer)
For composing multiple store enhancers, look at: http://redux.js.org/docs/api/compose.html