A look at the inner workings of Redux

Federico Knüssel
Feb 1, 2017 · 15 min read
http://redux.js.org/

TL;DR

The need for Redux

For communication between two components that don’t have a parent-child relationship, you can set up your own global event system. Subscribe to events in componentDidMount(), unsubscribe in componentWillUnmount(), and call setState() when you receive an event. The Flux pattern is one of the possible ways to arrange this.

https://css-tricks.com/learning-react-redux/

State Tree

Actions

function displayAlert() {
return {
type: 'DISPLAY_ALERT',
payload: {
message: 'Something went wrong'
}
};
}

Reducers

function someReducer(previousState, action) {
return nextState;
}

Store

store.getState()

console.log(store.getState()); // returns the state tree

store.dispatch(action)

store.dispatch({ type: 'SHOW_SPINNER' });

store.subscribe(callback)

const unsubscribe = store.subscribe(callback);unsubscribe();

Implementing createStore from scratch

const store = createStore(rootReducer);
function createStore(reducer) {
let state;
let listeners = [];

const getState = () => state;

const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
};

const subscribe = (listener) => {
listeners.push(listener);

return () => {
listeners = listeners.filter(l => l !== listener);
};
};

dispatch({ type: '@@redux/INIT' });

return {
getState,
dispatch,
subscribe
};
};

Reducer composition pattern

const initialState = {
count: 0,
alert: { visible: false, message: '' }
};
function rootReducer(state = initialState, action) {
switch(action) {
case 'COUNTER/INCREMENT':
return Object.assign({}, state, { count: state.count + 1 });
case 'COUNTER/DECREMENT':
return Object.assign({}, state, { count: state.count - 1 });
case 'ALERT/SHOW':
return Object.assign({}, state, {
alert: { visible: true, message: action.payload.message }
});
case 'ALERT/HIDE':
return Object.assign({}, state, {
alert: { visible: false, message: '' }
});
default:
return state;
}
}
const rootReducer = (state, action) => ({
count: counterReducer(state.counter, action),
alert: alertReducer(state.alert, action)
});
https://css-tricks.com/learning-react-redux/#article-header-id-7

A built-in reducer composition solution

import {combineReducers} from 'redux';
import {counter, alert} from './reducers';

const rootReducer = combineReducers({
count: counter,
alert
});
const combineReducers = (reducers) => {
return (state = {}, action) => {
return Object.keys(reducers).reduce((nextState, key) => {
nextState[key] = reducers[key](state[key], action);
return nextState;
}, {});
};
};

Presentational vs Container Components

Presentational components:

Container Components:

Connecting container components to the store

import {getState, dispatch, subscribe} from './store';

class Counter extends React.Component {
componentDidMount() {
this.unsubscribe = subscribe(this.forceUpdate);
}

componentWillUnmount() {
this.unsubscribe();
}

render() {
return (
<div>
<p>{getState().count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>
Increment counter
</button>
</div>
);
}
ReactDOM.render(
<App store={store} />,
document.querySelector('#root')
);
class Counter extends React.Component {
componentDidMount() {
const {subscribe} = this.props.store;
this.unsubscribe = subscribe(this.forceUpdate);
}

componentWillUnmount() {
this.unsubscribe();
}

render() {
const {getState, dispatch} = this.props.store;

return (
<div>
<p>{getState().count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>
Increment counter
</button>
</div>
);
}
}

Introducing react-redux

Using Provider to pass the store down to all children components

By adding childContextTypes and getChildContext to the context provider, React passes the information down automatically and any component in the subtree can access it by defining contextTypes. If contextTypes is not defined, then context will be an empty object. — React docs

export class Provider extends React.Component {
getChildContext() {
return {
store: this.props.store
};
}

render() {
return this.props.children;
}
}

Provider.childContextTypes = {
store: React.PropTypes.object.isRequired
};
class ChildComponent extends React.Component { ... }ChildComponent.contextTypes = {
store: React.PropTypes.object.isRequired
};

Generating container components with connect

export function connect(mapStateToProps, mapDispatchToProps) {
return function (WrappedComponent) {
class ConnectedWrappedComponent extends React.Component {
componentDidMount() {
const {subscribe} = this.context.store;

this.unsubscribe = subscribe(this.handleChange.bind(this));
}

componentWillUnmount() {
this.unsubscribe();
}

handleChange() {
this.forceUpdate();
}

render() {
const {getState, dispatch} = this.context.store;

return (
<WrappedComponent
{...this.props}
{...mapStateToProps(getState(), this.props)}
{...mapDispatchToProps(dispatch, this.props)} />
);
}
}

ConnectedWrappedComponent.contextTypes = {
store: React.PropTypes.object.isRequired
};

return ConnectedWrappedComponent;
};
}

mapStateToProps and mapDispatchToProps

function mapStateToProps(state) {
return {
count: state.counter
};
}
function mapDispatchToProps(dispatch) {
return {
increment: dispatch({ type: 'INCREMENT' }),
decrement: dispatch({ type: 'DECREMENT' })
};
}
export connect(mapStateToProps, mapDispatchToProps)(Counter);
function mapStateToProps(state, ownProps) { ... }
function mapDispatchToProps(dispatch, ownProps) { ... }

Using connect to generate a container component which is not connected to the store

const mapDispatchToProps = dispatch => ({ dispatch });export default connect(null, mapDispatchToProps)(Counter);
export default connect()(Counter);

Extracting action creators

function mapDispatchToProps(dispatch) {
return {
increment: () => dispatch(increment()),
decrement: () => dispatch(decrement())
};
};
export const increment = () => ({
type: 'COUNTER/INCREMENT'
});
export const decrement = () => ({
type: 'COUNTER/DECREMENT'
});
<button onClick={this.props.increment}>Increment</button>
<button onClick={this.props.dispatch({ type: 'INCREMENT' })}>
Increment
</button>

Credits

Federico Knüssel

Written by

Front End Developer 👨‍💻 🇦🇺

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade