Following up on the previous article, Getting Started with Redux: Basics (Part 1), we will now follow the documentation and cover the following three points:
- Data Flow
- Integrating with React
- Example: TodoList
Data Flow
Redux’s architecture revolves around establishing a strict unidirectional data flow.
This means all data follows the same lifecycle pattern, making the entire application predictable and easier to understand. In a Redux application, the data lifecycle is divided into the following four steps:
1 You Call store.dispatch(action)
An action is an object that describes what happened. For example:
{type:'ADD_TODO',text:'todo\'s content'}
{type: 'FETCH_USER_SUCCESS', response: {id: 3, name: 'Mary'}}
An action can be thought of as a brief, descriptive snippet of an event. You can call store.dispatch(action)
anywhere, including within components, XHR callbacks, or even timers.
2 The Redux Store Calls Your Provided Reducer Function
The store passes two arguments to the reducer: the current state and the action. For example, in a todo application, the root reducer would receive arguments similar to these:
let previousState = {
visibleTodoFilter: "SHOW_ALL",
todos: [
{
text: "Read the docs",
complete: false,
},
],
};
let action = {
type: "ADD_TODO",
text: "Understand the flow",
};
// nextState 由 todoApp 这个根 reducer 来生成
let nextState = todoApp(previousState, action);
It’s important to note that a reducer is a pure function with no side effects; it merely calculates the next state. It should be completely predictable, always returning the same value for the same arguments, no matter how many times it’s called. Operations like API calls or routing changes should occur before the action is dispatched.
3 The Root Reducer Can Combine the Outputs of Multiple Child Reducers to Generate a Single State Tree.
How you combine your root reducer is entirely up to you. Redux provides the combineReducers()
helper function, which is very useful when splitting your root reducer into independent functions, each managing a branch of the state tree.
Let’s look at how the combineReducers()
function works. Suppose you have two reducers: one to manage the todo list and another to manage the currently selected filter state:
function todos(state = [], action) {
// Somehow calculate it...
return nextState
}
function visibleTodoFilter(state = 'SHOW_ALL', action) {
// Somehow calculate it...
return nextState
}
let todoApp = combineReducers({
todos,
visibleTodoFilter
})
When an action is triggered, the todoApp
returned by combineReducers
will call all reducers:
let nextTodos = todos(state.todos, action)
let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action)
It then combines the state returned by each reducer into a single state tree:
return {
todos: nextTodos,
visibleTodoFilter: nextVisibleTodoFilter
}
You can also choose not to use combineReducers
; it’s just a convenient helper, and you can implement your root reducer yourself.
4 The Redux Store Saves the Entire State Tree Returned by the Root Reducer
This new state tree is now the application’s next state. All listener functions registered via store.subscribe(listener)
will be invoked; listeners may call store.getState()
to retrieve the current state.
Now, the entire view can be updated with the new state. If you are using React Redux as your view binding tool, this is where component.setState(newState)
would be called.
Integrating with React
While Redux has no direct ties to React, it works exceptionally well with frameworks like React and Deku that describe UI using data states. We will use React to build a simple Todo application.
Installing React Redux
React bindings are not included in Redux by default; we need to install them separately:
npm install --save react-redux
Presentational Components and Container Components
Redux’s React bindings advocate for the separation of presentational and container components.
--- | Presentational Components | Container Components |
---|---|---|
Purpose | How things look (markup, styles) | How things work (data fetching, state updates) |
Aware of Redux | No | Yes |
Reads data | From parent props | Subscribes to Redux state |
Modifies data | Calls callbacks from props | Dispatches Redux actions |
Are written | By hand | Usually generated by React Redux |
Most components should be written as presentational components, but we also need to generate some container components to connect them to the Redux store.
Technically, you can write container components by hand using store.subscribe()
. However, we don’t recommend this because React Redux performs many performance optimizations that are difficult to implement manually. Therefore, instead of writing container components by hand, we suggest generating them using the connect()
function provided by React Redux.
Designing Component Hierarchy
Our design is simple. We want to display a list of todo items. A button to mark a todo item as complete. A place to add new todos. In the footer, we need toggle buttons to show all, completed, or incomplete todos.
Presentational Components
With the following presentational components, we can outline a props hierarchy.
TodoList
is a list that displays available Todos.todos: Array
A list of items in the format{ id, text, completed }
.onTodoClick(id: number)
A callback function invoked when a todo is clicked.
Todo
A single todo item.text: string
The text to display.completed: boolean
The completion status of the todo.onClick
A callback function invoked when a todo is clicked.
Link
A link with a callback.onClick
The callback when this link is clicked.
Footer
Used to change the currently displayed todo items.App
The root component, used to render everything else.
They describe how the application looks but don’t know where the data comes from or how to change it. They simply render the data we give them. If you were to migrate from Redux to another framework, these components would likely remain almost unchanged. They have no direct connection to Redux.
Container Components
We also need some container components to connect the presentational components to Redux. For example, the TodoList
component needs a container VisibleTodoList
to subscribe to data from Redux and know how to use the current visibility filter. To change the visibility filter, we provide a FilterLink
container that renders a Link
which dispatches an appropriate action when clicked:
VisibleTodoList
filters the items to display based on the current visibility filter and renders aTodoList
.FilterLink
gets the current visibility filter and renders aLink
.filter: string
Represents a visibility filter.
Other Components
Sometimes, it’s hard to distinguish whether a component is presentational or a container. For example, forms and functions are sometimes interdependent, like in this small component:
AddTodo
An input field with an ‘Add’ button.
Technically, we could separate it into two components, but that would clearly be too cumbersome. When a project becomes complex and large, we can separate them, but for now, let’s keep them mixed.
Implementing Components
Presentational Components
These are just regular React components, so we won’t go into detail.
TodoList.js
import React, { PropTypes } from "react";
import Todo from "./Todo";
const TodoList = ({ todos, onTodoClick }) => (
<ul>
{todos.map((todo) => (
<Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)} />
))}
</ul>
);
TodoList.propTypes = {
todos: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired,
}).isRequired,
).isRequired,
onTodoClick: PropTypes.func.isRequired,
};
export default TodoList;
Container Components
Next, we will link presentational components with Redux by generating container components. Technically, a container component is just a React component that reads a part of the Redux state tree using store.subscribe()
and provides props to a presentational component for rendering. You can write this component by hand, but we recommend using the connect()
function provided by Redux to generate these container components, as it offers many useful optimizations to prevent unnecessary re-renders.
To use connect()
, you need to define a special function called mapStateToProps
, which specifies how to transform the current Redux store state into the props you want to pass to your presentational component. For example, VisibleTodoList
needs to compute todos
to pass to TodoList
, so we define a function to filter state.todos
by state.visibilityFilter
and use it in mapStateToProps
:
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case "SHOW_ALL":
return todos;
case "SHOW_COMPLETED":
return todos.filter((t) => t.completed);
case "SHOW_ACTIVE":
return todos.filter((t) => !t.completed);
}
};
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter),
};
};
In addition to reading state, container components can dispatch actions. Similarly, you can define a function named mapDispatchToProps()
that receives the dispatch()
method and returns callback props to inject into your desired presentational component. For example, we want VisibleTodoList
to inject a prop named onTodoClick
into the TodoList
component, and we want onTodoClick
to dispatch a TOGGLE_TODO
action:
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id));
},
};
};
Finally, we generate a VisibleTodoList
by calling connect()
and passing these two functions:
import { connect } from "react-redux";
const VisibleTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList);
export default VisibleTodoList;
These are the basic APIs of React Redux, but there are shortcuts and powerful options, so we encourage you to thoroughly review this documentation. If you’re concerned about the process of mapStateToProps
creating new objects, you might want to learn about computing derived data with reselect.
Passing the Store
All container components need to be connected to the Redux store so they can subscribe to it. One way is to pass it as a prop to every container component. However, that would be too cumbersome.
Our recommended approach is to use the special React Redux component <Provider>
to magically make the store available to all container components in the application without explicitly passing it. You only need to call it once when rendering the root component.
Index.js
import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";
import App from "./components/App";
import todoApp from "./reducers";
let store = createStore(todoApp);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root"),
);
Example: TodoList
Just reading the documentation, while introducing many technical terms and methods, still doesn’t fully clarify things. Therefore, it’s essential to look at the code. Here is the official TodoList source code, which, combined with these two articles, should provide a good understanding. I encountered a few issues here:
-
webpack kept reporting an error that it couldn’t find the entry file ’./index’. I eventually discovered a Babel pitfall. After Babel upgraded to 6.0, you need to specify the type of JavaScript files to compile in the
.babelrc
file; otherwise, compilation will fail.{ "presets": ["es2015", "react"] }
-
After the page rendered, clicking “add” and other actions worked correctly. However, clicking the filter buttons consistently threw errors, and the errors were inexplicable. So, I debugged step by step and finally discovered that the action corresponding to the filter in
Footer.js
had a typo, causing subsequent components to be unable to find the correct action.import React from "react"; import FilterLink from "../containers/FilterLink"; const Footer = () => ( <p> Show: <FilterLink filter="SHOW_ALL">All</FilterLink> {", "} <FilterLink filter="SHOW_ACTIVE">Active</FilterLink> {", "} <FilterLink filter="SHOW_COMPLETED">Completed</FilterLink> </p> ); export default Footer;
This shows that application actions still need to be maintained in a dedicated, separate file, where all actions are defined as variables and then exported. This way, any spelling errors can be caught during the compilation phase.
References
This article was published on May 3, 2016 and last updated on May 3, 2016, 3442 days ago. The content may be outdated.