Redux
Redux is a predictable state container for JavaScript apps.
As state in application becomes increasingly complicated for SAP, there might be more different types of state to manage: local created data which is not persisted to the server, UI data like UI elements properties, routes, spinners, paginations which determines the UI display; and server returned and cached data, etc. Redux attempts to make state mutations predictable for complicated application state by imposing certain restrictions on how and when updates can happen. These restrictions are reflected in the three principles of Redux.
Three principles of Redux
-
Single source of truth for application state: The global state of application is stored in an object tree within a single store;
-
State is read-only: The state cannot be modified directly by views or network callbacks, the right way to mutate is to dispatch an action to notify the reducer to process. As all changes are centralised and happen one by one in a strict order, there are no subtle race conditions to watch out for;
-
Changes are made with pure functions: Change state via reducers. A reducer is a pure function with two characteristics:
- It returns same result when the input parameters are the same; the only dependency of the returned data are the input parameters, and the result is consistent regardless of where/when it is called.
- no side-effects: the function will not do jobs like: modify external data, render DOM elements, execute network requests, IO operations.
These characteristics provides assurance to make state predictable.
Three core concepts of Redux
-
Actions: an action is a JavaScript object which defines what the action is, it must contain a
type
property to name the action, other properties on action object are defined according to the business scenarios. we usually create an action by writing an action creator function.123function
addAction() {
return
{ type:
'ACTION_TYPE_ONE'
, prop1:
'prop1-value'
, prop2:
'prop2-value'
}
}
-
Reducers: a reducer is a
pure function
which processes state when getting a dispatched action from components.1(state, action) => newState
The reducer function receives two arguments: the initial state(should be immutable) and the dispatched action, it returns the new state to store according to action types:
1234567891011121314function
reducer(state = initialState, action) {
switch
(action.type) {
case
ACTION_TYPE_ONE:
// avoid to modify the state directly, return a new state value via
// using ES6 spread operator(...) or function:
// Object.assign({}, state, prop1: new_value) function
return
{...state, prop1: new_value};
case
ACTION_TYPE_TWO:
return
{...state, prop2: new_value};
...
default
:
return
state;
}
}
When the state object becomes complex, we can divide actions by categories and assign different reducers to manage them.
redux
combine them into an entry/root reducer.1234567891011121314import
{ combineReducers } from
'redux'
;
// combineReducers return a boss reducer function
const rootReducer = combineReducers({
reducer1,
// manages state.categoryA of category A actions
reducer2
// manages state.categoryB of category B actions
});
// the above code is equivelent to the following
const rootReducer(state = {}, action) {
return
{
reducer1: reducer1(state.categoryA, action);
reducer2: reducer2(state.categoryB, action)
}
}
-
store:
store
is a JavaScript object which bridgesaction
andreducer
, it takes the following responsibilities:- saving and maintaining the application's global state;
- accessing application state via getState() function, new state can be used by useState() hook to render view;
- sending new action via dispatch(action) function;
- monitoring application state changes via subscribe(handler) function in component, handler is a callback function which always uses getState() to fetch new state and then does other page render;
12import
{ createStore } from
'redux'
;
let
store = createStore(rootReducer, initialState);
// create the App store
Data flow of Redux
components ----store.dispatch(action) send action ----> reducer(state, action) return state ----> store
Steps to use Redux
1 | npm install redux |
- create actions with unique type values
- create reducers for returning new state when receiving actions
- create a store to use reducers to manage state with actions
- register state monitor via store.subscribe(cb) in component
- send action by dispatch(action), the reducers will process the action and return a new state to store
- use getState() to fetch the new state as the state is already subscribed to be monitored by store.subscribe()
- unsubscribe the state monitor
Use Redux(react-redux) in React
react-redux
is a middleware providing more convenience for React.js apps to manage state in a redux
store. It's the official React bindings for Redux, integrated some encapsulated methods and providing better performance for using Redux.
1 | npm install redux react-redux |
-
Step 1: Create reducer(s): Create app reducer for processing actions and update state.
1234567// reducer.js
exports.reducer = (state = {count: 0}, action) {
switch
(action.type) {
case
:
'ADD_ACTION'
:
return
{ ...state, count: state.count + 1 }
default
:
return
state;
}
}
-
Step 2: Create and Provide the store to application components. By wrapping the application root component inside the
react-redux
Provider component, with the globalstore
as its prop, theProvider
component will expose store to all the application components. Thie principle of this implementation is:Provider
usesstore
as the prop and passes it down viacontext
, then all the components in the application can read the state, use the methods from store via context. It avoids the to pass down/import the state between components level by level manually.1234567891011121314151617181920//App.js
import
ReactDOM from
"react-dom"
;
import
rootReducer from
'./reducer'
import
ComA from
'./components/ComA'
import
ComB from
'./components/ComB'
import
{ createStore } from
'redux'
import
{ Provider } from
'react-redux'
let
store = createStore(rootReducer);
// create the store with reducer function
ReactDOM.render(
<Provider store={store}>
<React.Fragment>
<ComA />
<ComB />
</React.Fragment>
<Provider />,
document.getElementById(
"root"
)
);
-
Step 3: Connect the target component. The connect() method bridges a specific component to the store, it maps store state data, action functions as props to the target component and makes store data available to the target component.
12345678910111213141516171819202122232425//ComA.js: Component A
import
{ connect } from
'react-redux'
// receive dispatch method and use it
function
ComA({sendAction}) {
return
(
<button onClick={sendAction()}>Add(+)</button>
);
}
// action needs to be dispatched from current component
const mapDispatchToProps = (dispatch) => {
return
{
// this dispatch function will become a prop of current component,
// use this.props.sendAction() to dispatch action 'COMA_ACTION'
sendAction: () => {
dispatch({type:
'ADD_ACTION'
});
}
}
}
export
default
const connectedComp = connect(
null
,
mapDispatchToProps
// function used to add action dispatcher as current component's props
)(ComA);
// create a stateful component
123456789101112131415161718//ComB.js: Component B
import
{ connect } from
'react-redux'
function
ComB({state}) {
console.log(state);
// receive state
return
(
<h3> { state.count } </h3>
)
}
// use this function to make state be receivable from props
function
mapStateToProps(state, props) {
return
state;
// the returned state will be added as a prop of current component
}
export
default
const connectedComp = connect(
mapStateToProps,
// function used to add store state as current component's props
)(ComB);
// create a stateful component
-
Data flow of
react-redux
The follwing chart shows how the state transition between ComA and ComB:
Redux vs 'useReducer + useContext' hooks in React
The userReducer
hook has similar concepts(state, action, dispatch) to reducer in Redux. It allows us to manage multiple sub-values of state, especially when the new state depends on the previous one, it's a more powerful useState
hook, we can literally give it an alias like useMultipleStates
for better understand it.
1. userReducer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // ./hooks/reducer.js: reducer function export const SET_LOAD_STATUS = 'SET_OTHER_OVERALL' export const SET_COUNT_OVERALL = 'SET_CHINA_OVERALL' export default function reducer(state, action) { switch (action.type) { case SET_LOAD_STATUS: return { ...state, loaded: action.loaded }; case SET_COUNT_OVERALL: return { ...state, overall: action.overall }; default : throw new Error( <code>Unsupported action type: ${action.type}</code> ); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // ./hooks/useAppData.js: dispatch actions to reducer. import { useEffect, useReducer } from "react" ; import reducer, { SET_LOAD_STATUS, SET_COUNT_OVERALL,} from "./hooks/reducer" ; export default function useAppData(props) { const initialState = { loaded: false , overall: []}; const [state, dispatch] = useReducer(reducer, initialState); useEffect(() => { // data fetch or process before dispatching actions overall = ...; // dispatch actions dispatch({type: SET_LOAD_STATUS, loaded: true }); dispatch({type: SET_COUNT_OVERALL, overall: overall}) }, []); return state; } |
1 2 3 4 5 6 7 8 9 10 | // component.js: use state data managed by the reducer hook import updateState from './hooks/useAppData' ; import Loader from './components/Loader' ; import Overall from './components/Overall' ; export default function App() { const { state } = updateState(); <Loader status={ state.loaded } /> <Overall data={ state.overall } /> } |
2. useContext
Context is designed to share data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language.
There are two characteristics of this hook:
- When provided context in the original component changes, the component which uses the shared data will be rendered automatically.
- There is no need to pass data explicitly to the component props, the shared data is wrapped inside. This provides more flexibility to use shared data to sub-tree components, without restriction of the levels.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // Parent.js: create and provide shared data context import React, { createContext} from 'react' ; const Context = createContext(0); import Child from './component/Child' ; function Parent () { return ( <div> <Context.Provider value={count}> <Child /> </Context.Provider> </div> ) } function Child () { const count = useContext(Context); return ( <div>{ count }</div> ) } |
3. Combine useReducer and useContext to be a minimum Redux
The userReducer
hook provides centralised management of multiple states which can be applied to different sub components via props. However, when the states are used at deeper levels, passing data to props level by level seems tedious and not elegant.
We can get use of the feature of useContext
hook at this time to avoid this, as useContext
hook allows components on the sub-tree of their ancestor to use data via Context, which is provided by their ancestor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | // ./hooks/useAppData.js import { useEffect, useReducer, userContext } from "react" ; import reducer from "./hooks/reducer" ; const Context = createContext({}); // create export default function useAppData(props) { const initialState = { loaded: false , overall: []}; const [state, dispatch] = useReducer(reducer, initialState); return ( // share data via context <Context.Provider value={{state, dispatch: dispatch}}> <ChildA/> <ChildB/> </Context.Provider> ); } function ChildA () { const { state } = useContext(Context); // use context data return ( <div> <div>{ Context.state.count }</div> <button onClick={ () => Context.dispatch({ type: "INCREASE_COUNT" , count: state.count + 1 }) }> Increase Count </button> </div> ) } function ChildB () { return ( <div> <GrandSon /> </div> ); } function GrandSon () { const { state } = useContext(Context); // use context data return ( <div> Name: {state.grandson} </div> ) } |
From the above example:
- we use the reducer function to receive actions and manage state change by
userReducer
hook; - we expose state and dispatch method from the ancestor component to descendant components via the
context
provided byuseContext
hook; - sub-components consume the shared data and dispatch method via the provided
context
from ancestor component
We can say that by combination the feature of useReducer
and useContext
hooks, to some extent, it is feasible to manage the state as using Redux. And the whole process/steps/logic to share data seems similar in the background.
4. Does useReducer and useContext replace Redux?
Redux provides an application level state management solution, while useReducer
plus useContext
only provide the 'global' state management among sub-tree components of their ancestor component. We can certainly use the root component as the ancestor to make state more higher level and global, or create multiple reducers, create different contexts for different business logic components when the application logic and features become complex; however, these jobs are what Redux actually does, we should not stick to useReducer
and useContext
in this case, as Redux provides a more complete solutioin with other integrated features for us to manage state easier, safer and more efficiently. So, to summarise:
- Use useState for basic and simple/small size applications.
- Use useState + useReducer + useContext for medium size applications.
- Use useState/useReducer + Redux for complex/large size applications.
Comments