Notes On Redux
Table of Contents
Global Store, Event Listener, Dispatcher, Actions, Subscribe to store.
Reducer - function(state, action) { return modified state; } :
export default function(state = {}, action) {
switch (action.type) {
case DELETE_POST:
return _.omit(state, action.payload);
...
}
}
A combined reducer can handle any action by passing it through all.
Combined reducer creates separate name space with in global state :
postReducer = Operates on state to change any attribute. e.g. state.maxPosts = 10;
newsReducer = Operates on state to change any attribute. e.g. state.total = 20;
netReducer = combineReducers( { posts : postReducer, news : newsReducer } )
Operates on state.posts and state.news only.
Note that the combined reducer never messes up common state.
Dispatcher invokes the (combined) reducer and then all listeners. Each listener is a plain function without any args.
A store takes a (combined) reducer and initial state and gives you these handy little functions for you. You dont have to write these functions:
Synopsis :
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
//
// With Router ...
//
import { Router, Route } from 'react-router-dom'
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<Route exact path="/" component={App} />
<Route path="/foo" component={Foo} />
<Route path="/bar" component={Bar} />
</Router>
</Provider>,
document.getElementById('root')
)
The render function as a listener can be subscribed to global redux store. Then each time a global state changes, the component can be redrawn. That will be too many redraws.
Another option is to use reducer and dispatch action from child. To define such reducer, you need a handle to render function early on.
Typically reducer is used to alter global state. If all you want to do is to trigger action function, you can just remember the function handle in global state: e.g. :
store.getState().redrawAppFunc = renderApp; // Not recommended.
// Recommended to implement set logic in reducer ...
store.dispatch( { type: SET_REDRAW_FUNC, payload: renderApp })
// Call render on need basis from child ...
store.getState().redrawAppFunc();
// Helper function which transforms Object.
function mapValues(obj, fn) {
return Object.keys(obj).reduce((result, key) => {
result[key] = fn(obj[key], key);
return result;
}, {});
}
// Helper function to select subset of obj attributes using boolean func.
function pick(obj, fn) {
return Object.keys(obj).reduce((result, key) => {
if (fn(obj[key])) {
result[key] = obj[key];
}
return result;
}, {});
}
// Simple function compose -- Applied to auto bind the dispatch with Action
function bindActionCreator(actionCreator, dispatch) {
return (...args) => dispatch(actionCreator(...args));
}
export function bindActionCreators(actionCreators, dispatch) {
return typeof actionCreators === 'function' ?
bindActionCreator(actionCreators, dispatch) :
mapValues(actionCreators, actionCreator =>
bindActionCreator(actionCreator, dispatch)
);
}
// Given a list of functions, this returns single composed function.
export function compose(...funcs) {
return arg => funcs.reduceRight((composed, f) => f(composed), arg);
}
export function applyMiddleware(...middlewares) {
return (next) => (reducer, initialState) => {
var store = next(reducer, initialState);
var dispatch = store.dispatch;
var chain = [];
chain = middlewares.map(middleware => middleware({
getState: store.getState,
dispatch: (action) => dispatch(action)
}));
dispatch = compose(...chain)(store.dispatch);
return {...store,
dispatch
};
};
}
// Combined reducer can handle any action and return modified state!
export function combineReducers(reducers) {
var finalReducers = pick(reducers, (val) => typeof val === 'function');
return (state = {}, action) => mapValues(finalReducers,
(reducer, key) => reducer(state[key], action)
);
}
//
// The crux of the code.
// Input: A combined reducer, initialState
// Output:
// dispatch,
// subscribe,
// getState,
// replaceReducer
//
export function createStore(reducer, initialState) {
var currentReducer = reducer;
var currentState = initialState;
var listeners = [];
var isDispatching = false;
function getState() {
return currentState;
}
//
// A listener is a simple function without args.
// It will be called once for each dispatch of any action!
//
function subscribe(listener) {
listeners.push(listener);
return function unsubscribe() {
var index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}
//
// Compute current state; Then also call all listeners.
// Current reducer is a kind of listener. But you also have plain listeners.
//
function dispatch(action) {
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.');
}
try {
isDispatching = true;
currentState = currentReducer(currentState, action);
} finally {
isDispatching = false;
}
listeners.slice().forEach(listener => listener());
return action;
}
//
// Replace current reducer with new one.
//
function replaceReducer(nextReducer) {
currentReducer = nextReducer;
dispatch({
type: '@@redux/INIT' // Dummy action to force call all reducers again.
});
}
dispatch({
type: '@@redux/INIT' // Dummy Action: Useful to compute currentState.
});
return {
dispatch,
subscribe,
getState,
replaceReducer
};
}
// connect() is a function that injects Redux-related props into your component.
// You can inject data and callbacks that change that data by dispatching actions.
function connect(mapStateToProps, mapDispatchToProps) {
// It lets us inject component as the last step so people can use it as a decorator.
// Generally you don't need to worry about it.
return function (WrappedComponent) {
// It returns a component
return class extends React.Component {
render() {
return (
// that renders your component
<WrappedComponent
{/* with its props */}
{...this.props}
{/* and additional props calculated from Redux store */}
{...mapStateToProps(store.getState(), this.props)}
{...mapDispatchToProps(store.dispatch, this.props)}
/>
)
}
componentDidMount() {
// it remembers to subscribe to the store so it doesn't miss updates
this.unsubscribe = store.subscribe(this.handleChange.bind(this))
}
componentWillUnmount() {
// and unsubscribe later
this.unsubscribe()
}
handleChange() {
// and whenever the store state changes, it re-renders.
this.forceUpdate()
}
}
}
}
// This is not the real implementation but a mental model.
// It skips the question of where we get the "store" from
// (answer: <Provider> puts it in React context)
// The purpose of connect() is that you don't have to think about
// subscribing to the store or perf optimizations yourself, and
// instead you can specify how to get props based on Redux store state:
const ConnectedCounter = connect(
// Given Redux state, return props
state => ({
value: state.counter,
}),
// Given Redux dispatch, return callback props
dispatch => ({
onIncrement() {
dispatch({ type: 'INCREMENT' })
}
})
)(Counter)