##Intro to Redux
###Some important concepts of Redux
Reference: Three Principles, Basics
-
A single Store to maintain the state tree
- The state tree is a pure JS object.
- The shape of the Store should be designed top-down. You are always aware of the whole tree.
- The states tree should be normalized instead of deeply nested. An officially recommended approach: https://github.com/gaearon/normalizr
-
A single root Reducer to manage the states tree
- Reducers are pure functions which calculate a new state given an old state.
- You can have many reducers, each of which manages a small part of the state tree, and finally combine them into one root reducer. Redux offers a helper utility functions
combineReducers()
. - Reducers can both initialize (when old state is not given) and mutate (when old state is given) the state tree. But a reducer never mutate an object directly. Instead, it returns a new state without touching the old state.
Given the same arguments, it should calculate the next state and return it. No surprises. No side effects. No API calls. No mutations. Just a calculation.
- Many Actions to trigger the Reducers
- Actions are created by Action Creator functions
type
of an Action indicates which part of Reducer will be executed
store.dispatch(action)
-> store call reducer to update itself
Details explained here: http://redux.js.org/docs/basics/DataFlow.html
Redux architecture revolves around a strict unidirectional data flow.
###How Redux works with React
The package react-redux
provides a component wrapper called <Provider>
and an important helper function called connect
.
<Provider>
is used to connect React dom tree to store.
let store = createStore(rootReducer)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
connect
is used to bind data and functions to component. The following example is how connect
provide props
to a component TodoList
.
components/TodoList.js
:
Please note that TodoList
is not aware of Redux.
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
containers/VisibleTodoList.js
:
connect
takes two (actually four, but later two are less important) optional arguments. mapStateToProps
allows you to map data from the state tree to data needed in props
of the component. mapDispatchToProps
allows you to provide functions to props
, with access to store.dispatch
.
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
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)
}
}
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
##Thoughts on Redux
A global store made no sense to me when I first saw it, because you can always change something inside it secretly (it's not even Immutable) and the complexity of the state tree can explode very quickly. If all components rely on the same Store, the performance will be terrible.
However, following the convention of Redux:
- The state tree will never be updated without notifying the system.
- Components do NOT rely on the whole tree. Instead, using
mapStateToProps
, they only read some small pieces from the tree, and only be re-rendered when these pieces change. - When the state tree is normalized, it's no longer deeply nested. It means, you will not write a long chained statement like
state.todos[id].fields.author.history[id]
to read the data. And the performance of reducer will not get worse and worse.
A globally accessible Store certainly provides flexibility. For example, two components that are far away from each other in the dom structure can be bound to a same piece of data. Even if they both need to read and write the data, there's no need to worry about passing instances around. However, this may also cause hidden dependencies which are very hard to maintain. Here's the solution: treat your Store like it's your database in frontend.
Although not mentioned as a primary principal in the official docs, I believe it's a good practice to write getter functions for the state. Then we have APIs for both GET
method (getter functions) and POST/PUT/PATCH/DELETE
method (Actions describe the API and Reducers describe the behavior) to Store. A component will use getter functions to subscribe the data it needs into its props
, and call "API"s to update the Store. When you feel necessary to refactor the data structure, the only thing you need to make sure of is that the APIs are still functioning in the same ways. It should not be a hard thing to do for fine-grained APIs.
This is what I dislike about react-redux
:
-
They tell you it works in this way but no one explains the underlying mechanism.
-
connect
is an example of badly designed APIs. It takes four optional arguments.[mapStateToProps(state, [ownProps]): stateProps]
(Function)[mapDispatchToProps(dispatch, [ownProps]): dispatchProps]
(Object or Function)[mergeProps(stateProps, dispatchProps, ownProps): props]
(Function)[options]
(Object)
-
And everyone is using
mapDispatchToProps
in different ways.
- A globally accessible Store working as a database in frontend, with proper APIs associated, will provide good flexibility.
- Redux demands a strict holding of conventions. Any abuse of accessibility or bad implementation could break the unidirectional data flow.
- We don't have to use
react-redux
as long as we have a good replacement offering access to the Store,dispatch
, and a good way to do data mapping.