在 Redux 例子分析: todos 中,我从代码的角度来说明 todos 这个例子是怎样实现的。如果很了解redux,一目了然,但是如果不太熟悉,那么就觉得有些难度了。如果能从UI的角度来逐步分析,应该会更加直接。
运行 todos,做出如下效果:
实现Todo 列表
单条记录 Todo
先来分析一下 list 中的这两项,即 hello 和 goodbye。想要在浏览器里看到他们,对应的HTML要写成如下形式:
<li style="text-decoration: none;">hello</li>
<li style="text-decoration: line-through;">goodbye</li>
对于每一条记录,记作一个todo,把它写成一个 react 的 component:
const Todo = () => (
<li style="text-decoration: none;">hello</li>
)
和
const Todo = () => (
<li style="text-decoration: line-through;">goodbye</li>
)
可以看到,需要两个变量,一个表示text-decoration
,另一个表示 hello 或者 goodbye。这个 component 可以写成:
const Todo = ({ onClick, completed, text }) => (
<li
style={{
textDecoration: completed ? 'line-through' : 'none'
}}
>
{text}
</li>
)
Todo.propTypes = {
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}
为了能在 ‘line-through’ 和 ‘none’ 之间切换,Todo 还需要它的另外一个属性 onClick
:
const Todo = ({ onClick, completed, text }) => (
<li
onClick={onClick}
style={{
textDecoration: completed ? 'line-through' : 'none'
}}
>
{text}
</li>
)
Todo.propTypes = {
onClick: PropTypes.func.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}
记录列表 TodoList
如此,我们就得到我们想要的表示一条 todo 记录的 component。
接下来,todo 的列表如何生成?我想要的是这样的。
<ul>
<li .../>
<li .../>
<li .../>
<li .../>
</ul>
因为每个 <li .../>
对应一个 todo,那么,上面这个等价于:
<ul>
<Todo key=... onClick=... completed=... text=.../>
<Todo key=... onClick=... completed=... text=.../>
<Todo key=... onClick=... completed=... text=.../>
<Todo key=... onClick=... completed=... text=.../>
</ul>
这里的key
,onClick
,completed
和text
都可以从外部输入。每一条记录的onClick
的功能是相同的,所以可以使用同一个函数。
为每一条记录准备的数据的结构可以如下:
{
id: PropTypes.number.isRequired, // key
completed: PropTypes.bool.isRequired, // completed
text: PropTypes.string.isRequired // text
}
onTodoClick: PropTypes.func.isRequired // onClick
对于多条记录,可以转换成如下形式:
todos: arrayOf({
id: PropTypes.number.isRequired, // key
completed: PropTypes.bool.isRequired, // completed
text: PropTypes.string.isRequired // text
})
onTodoClick: PropTypes.func.isRequired // onClick
那么,如果已经有了输入数据,即有了 todos 和 onTodoClick,那么:
<ul>
<Todo key=... onClick=... completed=... text=.../>
<Todo key=... onClick=... completed=... text=.../>
<Todo key=... onClick=... completed=... text=.../>
<Todo key=... onClick=... completed=... text=.../>
</ul>
可以转换成:
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
/>
)}
</ul>
所以,表示todo 列表的 component 如下,它需要 todos
和 onTodoClick
两个属性:
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
}
到目前为止,这些都是关于 React 的。要想和 Redux 结合在一起,需要引入 react-redux 中的connect
模块,它的用法:
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
TodoList 与 redux-react 结合 - VisibleTodoList
通俗来说,就是通过mapStateToProps
和mapDispatchToProps
,把当前的 component TodoList
和 redux 连接起来。
引用 Redux 入门教程(三):React-Redux 的用法
mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。
mapStateToProps的第一个参数总是state对象,还可以使用第二个参数,代表容器组件的props对象。
// 容器组件的代码
// <FilterLink filter="SHOW_ALL">
// All
// </FilterLink>
const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
}
}
mapDispatchToProps是connect函数的第二个参数,用来建立 UI 组件的参数到store.dispatch方法的映射。也就是说,它定义了哪些用户的操作应该当作 Action,传给 Store。它可以是一个函数,也可以是一个对象。
如果mapDispatchToProps是一个函数,会得到dispatch和ownProps(容器组件的props对象)两个参数。
const mapDispatchToProps = (
dispatch,
ownProps
) => {
return {
onClick: () => {
dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: ownProps.filter
});
}
};
}
从上面代码可以看到,mapDispatchToProps作为函数,应该返回一个对象,该对象的每个键值对都是一个映射,定义了 UI 组件的参数怎样发出 Action。
如果mapDispatchToProps是一个对象,它的每个键名也是对应 UI 组件的同名参数,键值应该是一个函数,会被当作 Action creator ,返回的 Action 会由 Redux 自动发出。举例来说,上面的mapDispatchToProps写成对象就是下面这样。
const mapDispatchToProps = {
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
};
}
假设 Redux 已经帮我设定好了所有 state。格式: var state = { todos: [], visibilityFilter: '' }
一个 todos 中的元素,格式:{id: '', completed: '', text: ''}
visibilityFilter
是一个字符串,它有三个有效值:
‘SHOW_ALL’,’SHOW_COMPLETED’,’SHOW_ACTIVE’。
不同的值,可以对 todos 做不同类型的过滤:
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)
default:
throw new Error('Unknown filter: ' + filter)
}
}
我想通过 TodoList
来构造一个VisibleTodoList
,而TodoList
需要两个 属性:{ todos, onTodoClick }
。
通过mapStateToProps
得到属性todos
:
const mapStateToProps = (state) => ({
todos: getVisibleTodos(state.todos, state.visibilityFilter)
})
通过mapDispatchToProps
得到属性onTodoClick
:
const toggleTodo = (id) => ({
type: 'TOGGLE_TODO',
id
})
const mapDispatchToProps = {
onTodoClick: toggleTodo
}
如此,即可得到 VisibleTodoList
:
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)
default:
throw new Error('Unknown filter: ' + filter)
}
}
const mapStateToProps = (state) => ({
todos: getVisibleTodos(state.todos, state.visibilityFilter)
})
const mapDispatchToProps = {
onTodoClick: toggleTodo
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
ToggleTode 对应的 Action & Reducer
简单来说,在 Redux 中,用户定义一个 Action,然后一个对应的 Reducer。当调用 store.dispatch(action)
,对应的 Reducer 就会工作。
上面的 Action 已经定义好了:
export const toggleTodo = (id) => ({
type: 'TOGGLE_TODO',
id
})
对应的 Reducer:
const todos = (state = [], action) => {
switch (action.type) {
case 'TOGGLE_TODO':
return state.map(todo =>
(todo.id === action.id)
? {...todo, completed: !todo.completed}
: todo
)
default:
return state
}
}
要想将上面两个联系起来,还需要创建一个 store
:
const store = createStore(reducer)
可以直接将上面的 Reducer,即 todos
代替下面创建store
的reducer
。但是,通常情况下,我们并不会只有一个reducer
,而且从代码中看到,他的 state 也只是处理 todo 列表。所以我们肯定需要多个reducer
。即:
combineReducers({
todos,
...//其它 reducer
})
我们并没有给 todos
对应的名称,所有在 JS 中,上面等价于:
combineReducers({
todos: todos,
...//其它 reducer
})
所以,可以通过 state.todos
来访问它。
实现脚注 Show: All, Active, Completed
如果想要得到文章开头途中的脚注,需要的HTML应该是:
<p>
Show:
<span>All</span>
,
<a href="#">Active</a>
,
<a href="#">Completed</a>
</p>
Link
显然,每一条记录都可以用一个 component 来表示:
// active 表示是否激活,用来决定是用<span>还是<a>
// children 即sub component,在这里是文字
// onClick 表示如何响应
const Link = ({ active, children, onClick }) => {
if (active) {
return <span>{children}</span>
}
return (
// eslint-disable-next-line
<a href="#"
onClick={e => {
e.preventDefault()
onClick()
}}
>
{children}
</a>
)
}
Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired
}
FilterLink
我们需要从 Redux 获得 Link 所需的 active
和 onClick
。
// 获取active
const mapStateToProps = (state, ownProps) => ({
active: ownProps.filter === state.visibilityFilter
})
// 获取 onClick
const setVisibilityFilter = (filter) => ({
type: 'SET_VISIBILITY_FILTER',
filter
})
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => {
dispatch(setVisibilityFilter(ownProps.filter))
}
})
FilterLink 对应的 Action 和 Reducer
Action:
const setVisibilityFilter = (filter) => ({
type: 'SET_VISIBILITY_FILTER',
filter
})
Reducer:
const visibilityFilter = (state = 'SHOW_ALL', action) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
和前面的 Reducer 合并:
combineReducers({
todos,
visibilityFilter
})
可以通过store
的state.visibilityFilter
来访问属性。
增加记录
对应的HTML
<div>
<form>
<input>
<button type="submit">Add Todo</button>
</form>
</div>
AddTodo
在上面用connect
的时候,我们都传入了mapStateToProps
和mapDispatchToProps
作为参数,如果什么也不传入,还有一种形式可以使用 Redux 中的功能,直接在属性中写 dispatch
:
let AddTodo = ({ dispatch }) => {
let input
return (
<div>
<form onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
dispatch(addTodo(input.value))
input.value = ''
}}>
<input ref={node => {
input = node
}} />
<button type="submit">
Add Todo
</button>
</form>
</div>
)
}
AddTodo = connect()(AddTodo)
对应的Action 和 Reducer
Action addTodo:
let nextTodoId = 0
export const addTodo = (text) => ({
type: 'ADD_TODO',
id: nextTodoId++,
text
})
Reducer:
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
]
default:
return state
}
}
与 VisibleTodoList 的 Reducer 合并:
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
]
case 'TOGGLE_TODO':
return state.map(todo =>
(todo.id === action.id)
? {...todo, completed: !todo.completed}
: todo
)
default:
return state
}
}
如此,即可得到想要的效果。本来仅仅设计代码逻辑,代码如何组织请参考源代码。