Core Concepts of Redux
Redux is a standalone JavaScript library designed for predictable state management. Although it can integrate with any UI framework, it is predominantly paired with React to centralize shared states across multiple components.
When to Adopt Redux
- Multiple components require access to the same state.
- A component needs to modify the state of another component.
- General rule: Avoid Redux if local state suffices; adopt it only when state sharing becomes cumbersome.
Three Fundamental Principles
- Action: A plain JavaScript object describing an event. It must contain a
typeproperty (string identifier) and optionally apayload(data). - Reducer: A pure function that initializes state and computes the next state based on the previous state and the dispatched action.
- Store: The object that brings Actions and Reducers together. It holds the application state and provides methods:
getState(),dispatch(action), andsubscribe(listener).
Basic Redux Implementation
Consider a ScoreTracker component. We remove its local state and delegate it to Redux.
Constants and Actions
Define action types to prevent typos, then create action creators. For asynchronous operations, redux-thunk allows action creators to return a function instead of an object.
// state/constants.js
export const GAIN_POINTS = 'GAIN_POINTS';
export const LOSE_POINTS = 'LOSE_POINTS';
export const ADD_PLAYER = 'ADD_PLAYER';
// state/actions/scoreActions.js
import { GAIN_POINTS, LOSE_POINTS } from '../constants';
export const gainPoints = (qty) => ({ type: GAIN_POINTS, payload: qty });
export const losePoints = (qty) => ({ type: LOSE_POINTS, payload: qty });
export const gainPointsDelayed = (qty, ms) => (dispatch) => {
setTimeout(() => dispatch(gainPoints(qty)), ms);
}
// state/actions/playerActions.js
import { ADD_PLAYER } from '../constants';
export const recruitPlayer = (playerInfo) => ({ type: ADD_PLAYER, payload: playerInfo });
Reducers
Reducers must be pure functions. They should never mutate the incoming state directly.
// state/reducers/scoreReducer.js
import { GAIN_POINTS, LOSE_POINTS } from '../constants';
const initialScore = 0;
export default function scoreReducer(prevScore = initialScore, action) {
const { type, payload } = action;
switch (type) {
case GAIN_POINTS: return prevScore + payload;
case LOSE_POINTS: return prevScore - payload;
default: return prevScore;
}
}
// state/reducers/playerReducer.js
import { ADD_PLAYER } from '../constants';
const initialRoster = [{ id: '1', name: 'Alice', age: 25 }];
export default function playerReducer(prevRoster = initialRoster, action) {
const { type, payload } = action;
switch (type) {
case ADD_PLAYER: return [payload, ...prevRoster];
default: return prevRoster;
}
}
// state/reducers/index.js
import { combineReducers } from 'redux';
import score from './scoreReducer';
import players from './playerReducer';
export default combineReducers({ score, players });
Store Configuration
Combine reducers and apply middleware like redux-thunk and developer tools.
// state/store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from './reducers';
export default createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)));
Integrating with React using React-Redux
React-Redux provides an efficient way to connect React components to the Redux store, separating concerns into presentational (UI) and container components.
Provider and Store Injection
Wrap the root application component with <Provider> to pass the store automatically to all nested container components.
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './state/store';
import { Provider } from 'react-redux';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
Container Components
Use the connect function to map state and dispatch to props. mapDispatchToProps can be written as an object of action creators for brevity.
// containers/ScoreTracker.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { gainPoints, losePoints, gainPointsDelayed } from '../state/actions/scoreActions';
class ScoreUI extends Component {
addScore = () => {
const step = Number(this.stepInput.value);
this.props.gainPoints(step);
};
subScore = () => {
const step = Number(this.stepInput.value);
this.props.losePoints(step);
};
addIfOdd = () => {
const step = Number(this.stepInput.value);
if (this.props.currentScore % 2 !== 0) this.props.gainPoints(step);
};
addDelayed = () => {
const step = Number(this.stepInput.value);
this.props.gainPointsDelayed(step, 500);
};
render() {
return (
<div>
<h3>Current Score: {this.props.currentScore}</h3>
<h4>Team Size: {this.props.teamSize}</h4>
<select ref={c => this.stepInput = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.addScore}>+</button>
<button onClick={this.subScore}>-</button>
<button onClick={this.addIfOdd}>Add if Odd</button>
<button onClick={this.addDelayed}>Add Delayed</button>
</div>
);
}
}
export default connect(
(state) => ({ currentScore: state.score, teamSize: state.players.length }),
{ gainPoints, losePoints, gainPointsDelayed }
)(ScoreUI);
// containers/PlayerRoster.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { recruitPlayer } from '../state/actions/playerActions';
import { v4 as uuidv4 } from 'uuid';
class RosterUI extends Component {
addMember = () => {
const name = this.nameInput.value;
const age = this.ageInput.value;
this.props.recruitPlayer({ id: uuidv4(), name, age });
this.nameInput.value = '';
this.ageInput.value = '';
};
render() {
return (
<div>
<h3>Total Points: {this.props.totalPoints}</h3>
<input ref={c => this.nameInput = c} placeholder="Name" />
<input ref={c => this.ageInput = c} placeholder="Age" />
<button onClick={this.addMember}>Recruit</button>
<ul>
{this.props.roster.map(p => <li key={p.id}>{p.name} - {p.age}</li>)}
</ul>
</div>
);
}
}
export default connect(
(state) => ({ roster: state.players, totalPoints: state.score }),
{ recruitPlayer }
)(RosterUI);
Pure Functions Explained
A pure function consistently returns the same output for identical inputs. It must not cause side effects (like API calls) or mutate its arguments. Reducers in Redux must strictly be pure functions to ensure predictable state transitions.