The mapStateToProps / mapDispatchToProps Cheatsheet I Wish I Had In 2018

Every React + Redux codebase from before hooks revolves around connect. Three accordions of the wiring I keep paged in for inheriting one of those repos.

JavaScript
Frontend
3 snippets
react
design-patterns
state-machine
milozhang

By @milozhang

May 2, 2026

·

Updated May 20, 2026

722 views

8

4.2 (9)

// connect(mapStateToProps, mapDispatchToProps)(Component) is the canonical
// React-Redux idiom. mapState selects the slice the component cares about,
// mapDispatch builds bound action creators. Both run on every store update;
// connect only re-renders when the produced props shallow-differ.

// Tiny store stand-in. Real Redux is essentially this plus middleware.
function createStore(reducer, initialState) {
    let state = initialState;
    const listeners = [];
    return {
        getState: () => state,
        dispatch: (action) => {
            state = reducer(state, action);
            listeners.forEach((l) => l());
            return action;
        },
        subscribe: (l) => { listeners.push(l); return () => { const i = listeners.indexOf(l); if (i >= 0) listeners.splice(i, 1); }; },
    };
}

function shallowEqual(a, b) {
    if (a === b) return true;
    if (a == null || b == null) return false;
    const ak = Object.keys(a);
    if (ak.length !== Object.keys(b).length) return false;
    for (const k of ak) if (!Object.is(a[k], b[k])) return false;
    return true;
}

function connect(mapState, mapDispatch) {
    return function (Component) {
        return function Connected(ownProps, store) {
            const state = store.getState();
            const stateProps = mapState ? mapState(state, ownProps) : {};
            const dispatchProps = mapDispatch
                ? (typeof mapDispatch === 'function' ? mapDispatch(store.dispatch, ownProps) : mapDispatch)
                : { dispatch: store.dispatch };
            const merged = Object.assign({}, ownProps, stateProps, dispatchProps);
            return Component(merged);
        };
    };
}

// A tiny user reducer + a connected view.
function userReducer(state, action) {
    state = state || { name: 'Ada', online: false };
    if (action.type === 'SET_ONLINE') return Object.assign({}, state, { online: action.online });
    return state;
}
const store = createStore(userReducer, { name: 'Ada', online: false });

function UserBadge(props) {
    return props.name + ' is ' + (props.online ? 'online' : 'offline') + ' (toggle: ' + typeof props.toggle + ')';
}

const mapState = (state) => ({ name: state.name, online: state.online });
const mapDispatch = (dispatch) => ({
    toggle: (online) => dispatch({ type: 'SET_ONLINE', online }),
});

const ConnectedBadge = connect(mapState, mapDispatch)(UserBadge);
console.log(ConnectedBadge({}, store));
store.dispatch({ type: 'SET_ONLINE', online: true });
console.log(ConnectedBadge({}, store));

// Sanity: the dispatchProps function gives us bound action creators, so the
// view never imports the store or the action types directly.
const dispatchProps = mapDispatch(store.dispatch);
console.log('keys exposed to the view:', Object.keys(dispatchProps).join(', '));

The mental model that finally made connect click for me: think of the connected component as a function of (ownProps + storeSlice + boundActionCreators) -> reactTree. mapStateToProps projects the store down to the slice this view cares about, mapDispatchToProps builds action creators that already know how to call dispatch, and the merged object is what the component sees. The view is a pure function of those merged props, exactly like a hook-based component is a pure function of useSelector plus closed-over dispatchers. The optimisation that connect adds is a shallowEqual check on the produced state-props: the wrapped component only re-renders when one of the projected fields actually changed, which is the entire performance story for large connected apps.