1. Purpose
Like Flux, it serves as a state management layer that enforces strong constraints on unidirectional data flow.
2. Motivation
In MVC, there are clear boundaries between data (Model), presentation (View), and logic (Controller), but data flow is bidirectional, which is particularly evident in large-scale applications. A change (user input or internal API call) can affect the state in multiple parts of the application—as seen with two-way data binding—making maintenance and debugging difficult.
If one model can update another model, a view updating a model could lead to that model updating another, potentially triggering another view update. It becomes unclear what is happening in the application at any given moment because it's unknown when, why, and how state changes occur. The system becomes opaque, making it hard to reproduce bugs and add new features.
The goal is to reduce complexity and improve maintainability and code predictability by enforcing unidirectional data flow.
3. Core Concepts
Redux maintains the state of the entire application in a single immutable state tree. It cannot be changed directly; when changes occur, new objects are created via actions and reducers. Specifically:
-
The application's state object has no
settersand cannot be modified directly. -
State is modified by
dispatching actions. -
Reducersconnectactionsandstate. -
Higher-level
reducersorganize lower-level ones to form areducertree, calculating thestatelevel by level.
Functional reducers are key:
-
Small (single responsibility).
-
Pure (no side effects, no impact on the environment).
-
Independent (not environment-dependent; fixed input yields fixed output. Easy to test, only needing to focus on whether the return value for a given input is correct).
Pure function constraints enable powerful debugging features (otherwise state rollbacks would be nearly impossible), allowing precise tracking of changes via DevTools:
-
Displaying current
state, historicalactions, and their correspondingstates. -
Skipping certain
actionsto quickly assemble bug scenarios without manual preparation. -
State Reset, Commit, and Revert.
-
Hot loading to locate
reducerissues and apply fixes immediately.
4. Structure
action Like in Flux, these are events with a type and data (payload).
Similarly, actions are dispatched manually.
---
store Has the same function as in Flux, but there is only one globally.
Implemented as an immutable state tree.
Distributes actions and registers listeners. Each action passes through layers of reducers to yield a new state.
---
reducer Similar to arr.reduce(callback, [initialValue]).
The reducer acts as the callback, taking current state and an action as input and outputting a new state.
The concept of a reducer is similar to Node.js middleware or Gulp plugins. Each reducer is responsible for a small part of the state tree. A series of reducers are chained together (the output of the previous reducer serves as the input for the current one) to produce the final output state.
Every modification to the state by a reducer creates a new state object; old values point to the original reference, while new values are created anew.
Strict unidirectional data flow:
call new state
action --> store ------> reducers -----------> view
Actions are also handed to all top-level reducers (similar to Flux), flowing down to the appropriate subtrees.
The store coordinates the process: it passes the action and current state to the reducer tree to get the new state, updates the current state, and then notifies the view to update (in React, this would be setState()).
action
Actions describe what happened (like a news headline).
Action and action creator correspond to the traditional event and createEvent(). Action creators are used for portability and testability.
The architectural separation of action creator and store considers server-side rendering, where each request corresponds to an independent store, with the binding of action creator and store performed externally.
Note: In practice, creating an action and dispatching it should be decoupled. In scenarios where this is needed (e.g., passing to a child component while hiding the dispatch), Redux provides bindActionCreators to bind them together.
Additionally, consider asynchronous scenarios:
-
Number of
actionsAn asynchronous operation might require three
actions(or oneactionwith three states): Start/Success/Failure. These correspond to UI states: show loading / hide loading and show new data / hide loading and show error message. -
Timing of
viewupdatesAfter an asynchronous operation completes,
dispatch an actionto modify thestateand update theview.There is no need to worry about the timing of multiple asynchronous operations because, from the
actionhistory, the sequence is fixed regardless of whether thedispatchoccurred during a synchronous or asynchronous process.
This is not much different from synchronous scenarios, except there are more actions. Certain middleware (redux-thunk, redux-promise, etc.) just make asynchronous control look more elegant but are no different from the perspective of dispatching actions.
reducer
Responsible for specific state updates (updating state based on actions, turning the action's description into reality).
Compared to Flux, Redux uses pure function reducers instead of event emitters:
-
Decomposition and Composition
State is decomposed by splitting
reducers, which are then combined (using thecombineReducers()utility function) to form a state tree. Reducer composition is a common pattern (standard routine) in Redux applications.Typically, one
reduceris split into a group of similarreducers(or areducer factoryis abstracted). -
Single Responsibility
Each
reduceris responsible only for a portion of the global state.
Specific constraints for a pure function reducer (consistent with the concept of pure functions in FP) are as follows:
-
Do not modify arguments.
-
Perform pure calculations only; do not include side effects like routing changes or other API calls.
-
Do not call impure methods (where output depends on environment as well as input), such as
Math.random()ornew Date().
Furthermore, reducers are closely tied to state. Since state is the result of the reducer tree's calculations, the entire application's state structure must be planned first. Here are some very useful tips:
-
Split
stateinto data state and UI state.UI state can be maintained within components or attached to the state tree, but distinguishing between data state and UI state is always worth considering.
(Simple scenarios and UI state changes might not need to be part of the
storeand should be maintained at the component level instead.) -
Treat
stateas a database.For complex applications, treat the
stateas a database. Index data when storing it, and use IDs to reference related data. This keeps things independent and reduces nested states (nested states makestatesubtrees larger, whereas adata table + relationship tableapproach does not).
Store
The glue that organizes actions and reducers and supports listeners.
Responsible for three things:
-
Holding the
stateand supporting read/write access (getState()to read,dispatch(action)to write). -
Scheduling the
reducerwhen anactionis received. -
Registering/unregistering
listeners(triggered on every state change).
5. Three Basic Principles
The entire application corresponds to a single state tree
This makes it very easy to generate another state (keeping historical versions) and implement redo/undo.
State is read-only
-
Statecan only be updated by triggering anaction. -
Changes are centralized and occur in a strict sequence (no race conditions to worry about).
-
Since
actionsare plain objects, they can be logged, serialized, stored, and replayed later (for debugging/testing).
Reducers are pure functions
They take state and an action as input and output a new state. They always return a new object without maintaining (modifying) the input state.
Thus, reducer execution order can be adjusted at will, enabling movie-like debugging controls.
6. react-redux
Redux has no relationship with React; as a state management layer, it can be used with any UI solution, such as Backbone, Angular, React, etc.
react-redux handles the new state -> view part. That is, once there is a new state, how is the view synchronized?
container
It also uses the concepts of container and view (same as Flux).
A container is a special type of component without view logic that is closely tied to the store. Logically, it reads a portion of the state tree via store.subscribe() and passes it as props to the ordinary components (view) below.
connect()
A seemingly magical API that primarily does three things:
-
Injects
dispatchandstatedata aspropsinto the ordinary components below. -
Automatically inserts
containersinto the Virtual DOM tree. -
Includes built-in performance optimizations to avoid unnecessary updates (built-in
shouldComponentUpdate).
7. Redux vs. Flux
Similarities
-
Both extract Model update logic into a separate layer (Redux's
reducer, Flux'sstore). -
Neither allows direct
modelupdates, requiring anactionto describe every change. -
The basic approach of
(state, action) => stateis consistent.
Differences
-
Redux is a specific implementation, while Flux is a pattern.
There is only one Redux, whereas Flux has over a dozen implementations.
-
Redux's
stateis a single tree.Redux hangs the application state on one tree, with only one global
store.Flux has multiple
storesand broadcasts state changes as events; components sync their current state by subscribing to these events. -
Redux has no concept of a
dispatcher.Because it relies on pure functions instead of event emitters. Pure functions can be combined freely without needing to manage sequence explicitly.
In Flux, the
dispatcheris responsible for passingactionsto allstores. -
Redux assumes
stateis not manually modified.A moral constraint; modifying
statewithin areduceris not allowed (new properties can be added, but existing ones cannot be modified).Not making this a hard constraint considers certain performance scenarios; technically, it can be bypassed by writing impure
reducers.However, if a
reduceris impure, the powerful debugging features relying on pure function composition will be broken, so this is strongly discouraged.The reason
stateis not forced to use immutable data structures is for performance (extra overhead related to immutability) and flexibility (it can be used withconst,immutablejs, etc.).
8. Questions and Reflections
1. How is granularity controlled in the state change subscription mechanism?
Since subscribe(listener) only yields the complete global state, what is the granularity of React's setState()? How are subtrees handled?
It's handled manually. Any change to the state tree notifies all listeners, which then manually determine if the specific part of the state they care about has changed. In other words, the subscription mechanism doesn't handle distribution; manual distribution is required.
2. What's the deal with react-redux's <Provider>?
I'll guess it's some black magic performed via (Guessed wrong.) It requires hostContainerInfo.Provider to be the top-level container when rendering root:
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
hostContainerInfo looks like this:
function ReactDOMContainerInfo(topLevelWrapper, node) {
var info = {
_topLevelWrapper: topLevelWrapper,
_idCounter: 1,
_ownerDocument: node ? node.nodeType === DOC_NODE_TYPE ? node : node.ownerDocument : null,
_node: node,
_tag: node ? node.nodeName.toLowerCase() : null,
_namespaceURI: node ? node.namespaceURI : null
};
if ("development" !== 'production') {
info._ancestorInfo = node ? validateDOMNesting.updatedAncestorInfo(null, info._tag, null) : null;
}
return info;
}
(Taken from ReactDOM v15.5.4 source code)
All components on the Virtual DOM tree share hostContainerInfo, so the store is accessible in all containers. For example code, see Usage with React.
Actual implementation of react-redux
I guessed wrong; take a look here.
Internal instances are private properties (a random key, __reactInternalInstance&<random>), so components cannot access hostContainerInfo. However, React provides an enhanced version of hostContainerInfo called context, specifically for scenarios requiring deep manual passing of props. It roughly works like this:
// Provider
class Provider extends React.Component {
constructor(props) {
super(props);
}
// Use the manually passed store prop as a context property
getChildContext() {
return {store: this.props.store};
}
render() {
return this.props.children;
}
}
// container
class Container extends React.Component {
// Retrieve the store from context and use it as a container prop
// The store is then accessible via this.props.store within the container
getDefaultProps() {
return {
store: this.context.store;
}
}
}
It works as if the store penetrates from the top level into all components. Technically, ordinary components (view, not container) could also access the store directly via this.context.store (since context is passed down automatically and mindlessly), but doing so would be improper.
P.S. I never knew what context was for; now I finally understand.
3. How are tree scenarios (infinite expansion) handled?
In a typical business scenario involving an infinite tree structure, the trick is to treat state as a database (as mentioned earlier).
According to Redux philosophy, the tree should be flattened into nodes. Coarse granularity could be nodeId - children, while fine granularity would be nodeId - node (where children becomes a childrenIdList, and children are retrieved by looking up the master ID table).
Flattening solves the problem and is much easier to maintain than nested state. If a tree component corresponded to a single tree object (with all nodes on the tree), performing partial updates on a large tree would be very painful.
P.S. I can't believe 3NF can be applied to the front end!
References
-
Redux doc: Excellent documentation; I couldn't stop reading it.
No comments yet. Be the first to share your thoughts.