Redux

Redux Basics

What is Redux?

Redux is a library for managing and updating application state, using events called “actions”. It serves as a centralized store for state that needs to be used across your entire application, with rules ensuring that the state can only be updated in a predictable fashion.

The patterns and tools provided by Redux make it easier to understand when, where, why, and how the state in your application is being updated, and how your application logic will behave when those changes occur.

When to use Redux

  • You have large amounts of application state that are needed in many places in the app
  • The app state is updated frequently over time
  • The logic to update that state may be complex
  • The app has a medium or large-sized codebase, and might be worked on by many people

Three Principles

Redux can be described in three fundamental principles:

  • Single source of truth The state of your whole application is stored in an object tree within a single store
  • State is read-only The only way to change the state is to emit an action, an object describing what happened
  • Changes are amde with pure function To specify how the state tree is transformed by actions, you write pure reducers

Installation

To use Redux, need to first install redux and react-redux

1
npm install redux react-redux

Counter Example without Redux

We can convert a Counter component from using state to use Redux to store its state.

Counter component that doesn’t use Redux:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { useState } from "react";

function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
return (
<div>
<div>
<input type="text" placeholder="step" value={step} onChange={ e => setStep(parseInt(e.target.value)) } />
</div>
<div>
<button onClick={() => setCount(count + step)}>Increase</button>
</div>
<div>
<button onClick={() => setCount(count - step)}>Decrease</button>
</div>
<p>{count}</p>
</div>
);
}

export default Counter;

Currently the count is a local state to the Counter component, we will move the count to Redux store and create actions and reducers to change this state.

Actions

An action is a plain JavaScript object that has a type field. You can think of an action as an event that describes something that happened in the application. Actions usually contains payloads that is needed to update the state.

An action creator is a function that creates and returns an action object. We typically use these so we don’t have to write the action object by hand every time:

actions/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export const increase = step => ({
type: "INCREASE",
payload: {
step
}
});

export const decrease = step => ({
type: "DECREASE",
payload: {
step
}
});

Here we define two action creators, increase and decrease.

Reducers

A reducer is a function that receives the current state and an action object, decides how to update the state if necessary, and returns the new state: (state, action) => newState.

Reducers must always follow some specific rules:

  • They should only calculate the new state value based on the state and action arguments
  • They are not allowed to modify the existing state. Instead, they must make immutable updates, by copying the existing state and making changes to the copied values.
  • They must not do any asynchronous logic, calculate random values, or cause other “side effects”

reducers/counter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const initialState = {
value: 0
};
const counter = (state = initialState, action) => {
switch (action.type) {
case "INCREASE":
return { ...state,
value: state.value + action.payload.step
};
case "DECREASE":
return { ...state,
value: state.value - action.payload.step
};
default:
return state;
}
};
export default counter;

Here the initialState is a javascript object that contains a number.

Redux expects that all state updates are done immutably. so you need to return a new object/array instead of modifying the existing state.

There are usually multiple Reducers, we can use combineReducers function to combine multiple reducers.

reducers/index.js

1
2
3
4
5
6
import { combineReducers } from 'redux'
import counter from './counter'

export default combineReducers({
counter
})

Store

The current Redux application state lives in an object called the store.

The store is created by passing in a reducer.

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { createStore } from 'redux'
import { Provider } from 'react-redux'

import rootReducer from './reducers'

const store = createStore(rootReducer)

ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);

Optional: To use Redux with Redux DevTools Extension, you need to add an extract argument to createStore function.

1
2
3
4
const store = createStore(
reducer, /* preloadedState, */
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

Dispatch

The Redux store has a method called dispatch. The only way to update the state is to call store.dispatch() and pass in an action object. You can think of dispatching actions as “triggering an event” in the application.

One way to use dispatch is to use useDispatch hook.

1
const dispatch = useDispatch();

Counter Component

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
43
import React, { useState } from "react";

import { connect } from "react-redux";

import { increase, decrease } from "../actions";

function Counter(props) {
const [step, setStep] = useState(1);
return (
<div>
<div>
<input
type="text"
placeholder="step"
value={step}
onChange={e => setStep(parseInt(e.target.value, 10))}
/>
</div>
<div>
<button onClick={() => props.incr(step)}>Increase</button>
</div>
<div>
<button onClick={() => props.decr(step)}>Decrease</button>
</div>
<p>{props.count}</p>
</div>
);
}

const mapStateToProps = state => {
return { count: state.counter.value };
};

const mapDispatchToProps = dispatch => {
return {
incr: step => dispatch(increase(step)),
decr: step => dispatch(decrease(step))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter);

React Redux provides a connect function for you to read values from the Redux store and dispatch actions. connect function accepts two parameters.

  • mapStateToProps - function that transforms the current Redux store state into props of the component
  • mapDispatchToProps - function or object. If it is a function. it should return an object full of functions that use dispatch to dispatch actions. If it is an object, it will contain functions that use dispatch to dispatch actions.

Dataflow

Initial setup: A redux store is created with a root reducer function.

Update

  1. When user clicks a button, app code dispatch an action to the Redux store. {type: 'INCREASE', step: 2}
  2. Reducer function runs with the previous state and current action. Return value as the new state.
  3. Store notify all parts of the UI that are subscribed that the store has been updated
  4. UI component re-render with the new state.

Counter Example with Redux

Here is the complete code for Counter example that uses Redux

Reference