@stackmeister/react-reduce-context
Poor mans redux.
Whenever Redux feels like too much for now.
Install
// Yarn
yarn add @stackmeister/react-reduce-context
// NPM
npm i @stackmeister/react-reduce-context
TypeScript typings are included (No @types/ package needed)
Usage
Basic usage
Create a store slice. This is a completely isolated module.
import { createReduceContext } from '@stackmeister/react-reduce-context'
const { useContext, Provider } = createReduceContext({
initialState: {
counter: 0,
loading: false,
},
reduce: (state, action) => {
switch (action.type) {
case 'reset': {
return {
...state,
counter: 0,
}
}
case 'set': {
return {
...state,
counter: action.value,
}
}
case 'increase': {
return {
...state,
counter: state.counter + action.amount,
}
}
case 'decrease': {
return {
...state,
counter: state.counter - action.amount,
}
}
case 'startLoading': {
return {
...state,
loading: true,
}
}
case 'finishLoading': {
return {
...state,
counter: action.value,
loading: false,
}
}
}
},
dispatchers: {
reset: dispatch => {
dispatch({ type: 'reset' })
},
set: (dispatch, amount) => {
dispatch({ type: 'set' })
},
increase: (dispatch, amount) => {
dispatch({ type: 'increase', amount })
},
decrease: (dispatch, amount) => {
dispatch({ type: 'decrease', amount })
},
fetchValue: async dispatch => {
dispatch({ type: 'startLoading' })
const value = await new Promise(resolve => {
setTimeout(() => resolve(1337), 2000)
})
dispatch({ type: 'finishLoading', value })
},
}
methods: {
double: (state, { set }) => {
set(state.counter * 2)
},
pow: (state, { set }, exponent) => {
set(Math.pow(state.counter, exponent))
}
},
getters: {
counterTimesTwo: state => {
return state.counter * 2
}
},
})
export const useCounter = useContext
export const CounterProvider = Provider
Use it
import { useCounter, CounterProvider } from './utils/counter'
const Counter = () => {
const {
state,
increase,
fetchValue,
pow,
counterTimesTwo,
} = useCounter()
return (
<div>
Counter value: {{ state.counter }}
{state.loading && <span>Counter is loading...</span>}
{/* Call dispatchers */}
<button onClick={() => increase(1)}>Increase by 1</button>
<button onClick={() => increase(5)}>Increase by 5</button>
<button onClick={() => fetchValue()}>Fetch external</button>
{/* Call methods */}
<button onClick={() => pow(2)}>Double</button>
{/* Retrieve getters */}
Counter times two: {{ counterTimesTwo }}
</div>
)
}
const App = () => {
return (
<CounterProvider>
<Counter />
</CounterProvider>
)
}
render(<App />, document.getElementById('root'))
Compose multiple slices
Combination of two slices of state right now is rather manual, but works well.
With some utilities added to this library it can be easily supported, but
existing concepts are not fleshed out well enough yet.
export default {
initialState: {
list: [],
loading: false,
},
reduce: reduceUsers,
dispatchers: {
loadUsers: async dispatch => {
dispatch({ type: 'startLoading' })
const users = await fetchUsersFromApi()
dispatch({ type: 'finishLoading', users })
}
}
}
export default {
initialState: {
list: [],
loading: false,
},
reduce: reduceGroups,
dispatchers: {
loadGroups: async dispatch => {
dispatch({ type: 'startLoading' })
const groups = await fetchGroupsFromApi()
dispatch({ type: 'finishLoading', groups })
}
}
}
import users from './state/users'
import groups from './state/groups'
const modules = { users, groups }
const initialState = Object.fromEntries(
Object.entries(modules)
.map(([key, module]) => [key, module.initialState])
)
const reducers = Object.fromEntries(
Object.entries(modules)
.map(([key, module]) => [key, module.reduce])
)
const reduce = (state, action) => {
return {
...state,
[action.key]: reducers[action.key](action.action),
}
}
const wrapDispatchers = (key, dispatchers) =>
Object.fromEntries(
Object.entries(dispatchers)
.map(([key, dispatcher]) =>
[key, (dispatch, ...args) => dispatcher(action => dispatch({ key, action }), ...args)]
)
)
const dispatchersList = Object.entries(modules)
.map(([key, dispatchers]) => wrapDispatchers(key. dispatchers))
const dispatchers = Object.assign({}, ...dispatchersList)
const { useContext, Provider } = createReduceContext({
initialState,
reduce,
dispatchers,
})
export const useAppState = useContext
export const AppStateProvider = Provider
And this is how you would use it inside a component:
const { state: { users }, loadUsers } = useAppState()
useEffect(() => {
loadUsers()
}, [])
return (
<div>
{users.loading && <div>Users are loading currently...</div>}
{users.items.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
)
Of course, other sophisticated and more complex setups are possible, e.g.
dispatchers grouped by their module key so that they don't overlap etc.
Exactly because of this there is no fixed solution implemented for
composed modules right now, but its still possible to construct
such a system with the building blocks provided by this library.