Store
在前面的章节中,我们定义了代表“发生了什么”的事实的actions以及根据这些动作更新状态的Reducer。
Store是将他们聚集在一起的对象。Store有以下责任:
- 持有应用程序状态;
- 允许通过getState()访问状态;
- 允许状态通过dispatch(action)更新;
- 通过订阅注册subscribe(listener);
- 通过subscribe(listener)返回的函数来处理注销监听器。
重要的是要注意,你将只有一个store在Redux应用程序中。当你想拆分你的数据处理逻辑时,你将使用reducer组合而不是许多store。
如果您有reducer,创建store很容易。在上一节中,我们使用了combineReducers()将多个reducer合并为一个。我们现在将其导入,并将其传递给createStore()。
123 | import { createStore } from 'redux'import todoApp from './reducers'const store = createStore(todoApp) |
您可以选择指定初始状态作为createStore()的第二个参数。这对于保证客户端的状态与服务器上运行的Redux应用程序的状态相匹配非常有用。
1 | const store = createStore(todoApp, window.STATE_FROM_SERVER) |
Dispatching Actions
现在我们已经创建了一个store,让我们来验证我们的程序的作品!即使没有任何UI,我们也可以测试更新逻辑。
1234567891011121314151617181920212223242526 | import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters} from './actions'console.log(store.getState())// Every time the state changes, log it// Note that subscribe() returns a function for unregistering the listenerconst unsubscribe = store.subscribe(() => console.log(store.getState()))// Dispatch some actionsstore.dispatch(addTodo('Learn about actions'))store.dispatch(addTodo('Learn about reducers'))store.dispatch(addTodo('Learn about store'))store.dispatch(toggleTodo(0))store.dispatch(toggleTodo(1))store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))// Stop listening to state updatesunsubscribe() |
你可以看到这是如何导致store的状态发生变化的:

在我们开始编写UI之前,我们指定了我们的应用程序的行为。我们不会在本教程中做到这一点,但是现在您可以为您的reducer和actionCreator编写测试。你不需要嘲笑任何东西,因为它们只是纯粹的功能。调用他们,并就他们返回的内容作出断言。
index.js
1234 | import { createStore } from 'redux'import todoApp from './reducers'const store = createStore(todoApp) |
Data Flow
Redux体系结构围绕严格的单向数据流进行。
这意味着应用程序中的所有数据都遵循相同的生命周期模式,使您的应用程序的逻辑更具可预测性并更易于理解。它还鼓励数据规范化,以便最终不会出现多个相互不知道的相同数据的独立副本。
如果您仍然不确定,请阅读Motivation和The Case for Flux,以获得支持单向数据流的引人注目的论点。虽然Redux不完全是Flux,但它具有相同的关键优势。
任何Redux应用程序中的数据生命周期都遵循以下4个步骤:
1.你可以调用store.dispatch(action)
动作是一个描述发生的事情的简单对象。例如:
123 | { type: 'LIKE_ARTICLE', articleId: 42 } { type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } } { type: 'ADD_TODO', text: 'Read the Redux docs.' } |
把行动看作是一个非常简短的新闻片段。“玛丽喜欢第42条”或“阅读Redux文档”。被添加到todos列表中。“
您可以从应用程序中的任何位置调用store.dispatch(action),包括组件和XHR回调,或者甚至以预定的时间间隔。
2.Redux store调用您提供的reducer功能。
store将向reducer传递两个参数:当前状态树和动作。例如,在todo应用程序中,根reducer可能会收到如下所示的内容:
12345678910111213141516171819 | // The current application state (list of todos and chosen filter) let previousState = { visibleTodoFilter: 'SHOW_ALL', todos: [ { text: 'Read the docs.', complete: false } ] } // The action being performed (adding a todo) let action = { type: 'ADD_TODO', text: 'Understand the flow.' } // Your reducer returns the next application state let nextState = todoApp(previousState, action) |
请注意,reducer是一个纯功能。它只计算下一个状态。它应该是完全可预测的:多次调用相同的输入应该产生相同的输出。它不应该执行API调用或路由器转换等任何副作用。这些应该在动作发出之前发生。
3.根reducer可以将多个reducer的输出组合成单个状态树。
您如何构建根部reducer完全取决于您。Redux提供了combineReducers()辅助函数,用于将根reducer“分解”为单独的函数,每个函数管理状态树的一个分支。
这是combineReducers()的工作原理。假设您有两个reducer,一个用于todos列表,另一个用于当前选择的过滤器设置:
1234567891011121314 | function (state = [], action) { // Somehow calculate it... return nextState}function visibleTodoFilter(state = 'SHOW_ALL', action) { // Somehow calculate it... return nextState}let todoApp = combineReducers({ todos, visibleTodoFilter}) |
当你发出一个动作时,由combineReducers返回的todoApp将调用两个reducer:
12 | let nextTodos = todos(state.todos, action)let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action) |
然后它会将两组结果合并为一个状态树:
1234 | return { todos: nextTodos, visibleTodoFilter: nextVisibleTodoFilter} |
虽然combineReducers()是一个方便的助手实用程序,但您不必使用它;随时写你自己的根reducer!
4.Redux存储保存由根reducer返回的完整状态树。
这棵新树现在是你的应用程序的下一个状态!现在每个用store.subscribe(listener)注册的监听器都会被调用;监听器可以调用store.getState()来获取当前状态。
现在,UI可以更新以反映新的状态。如果使用React Redux之类的绑定,则这是调用component.setState(newState)的点。
React
从一开始,我们需要强调Redux与React没有任何关系。您可以使用React,Angular,Ember,jQuery或vanilla JavaScript编写Redux应用程序。
也就是说,Redux与React和Deku等库合作得非常好,因为它们让您将UI描述为状态的函数,Redux响应操作发出状态更新。
我们将使用React来构建我们简单的待办事项应用程序。
React Redux install
React绑定不包含在默认的Redux中。你需要明确地安装它们:
1 | npm install --save react-redux |
如果您不使用npm,您可以从unpkg获取最新的UMD版本(开发版或生产版)。如果您通过script标记将其添加到您的页面,则UMD构建会导出一个名为window.ReactRedux的全局。
展示和容器组件
Redux的React绑定包含分离表示和容器组件的想法。如果您不熟悉这些条款,请先阅读有关条款,然后再回来。他们很重要,所以我们会等待!
完成阅读文章?我们来重述一下他们的区别:
展示 Components | 容器 Components | |
---|---|---|
含义 | 看起来如何(标记,样式) | 工作原理(数据读取,状态更新) |
Redux | No | Yes |
读取数据 | 从props读取数据 | 订阅Redux状态 |
改变数据 | 从props调用回调 | Dispatch Redux actions |
写入 | 手动写 | 通常由React Redux生成 |
我们要编写的大多数组件都是展示的,但我们需要生成一些容器组件以将它们连接到Redux存储。这和下面的设计概要并不意味着容器组件必须靠近组件树的顶部。如果一个容器组件变得太复杂了(即它有很多嵌套的展示组件,并有无数的回调被传递下去),那么在组件树中引入另一个容器,如FAQ中所述。
从技术上讲,你可以用store.subscribe()手工编写容器组件。我们不建议您这样做,因为React Redux会进行许多难以完成的性能优化。出于这个原因,我们将使用React Redux提供的connect()函数生成它们,而不是编写容器组件,如下所示。
设计组件层次结构
请记住我们如何设计根状态对象的形状?是时候我们设计UI层次结构来匹配它。这不是特定于Redux的任务。在React中思考是一个很好的教程,可以解释这个过程。
我们的设计简介很简单。我们想要显示待办事项列表。单击时,待办事项完成后划掉。我们想要显示一个字段,用户可以添加新的待办事项。在底部,我们想要显示切换显示全部,只显示完成或仅显示活动待办事项。
设计演示组件
我看到以下演示组件和它们的props出现在这个简短的介绍中:
- ToDoList 是一个列表,显示可见的待办事项。
- todos:Array是包含{id,text,completed}形状的todo项目的数组。
- onTodoClick(id:number)是单击todo时调用的回调。
- ToDo是一个单一的待办事项
- text: string是要显示的文字。
- completed: boolean是待办事项是否应该出现划掉。
- onClick()是单击todo时调用的回调。
- Link是一个回调链接。
- onClick()是单击链接时调用的回调。
- Footer是我们让用户更改当前可见的待办事项的地方。
- App是呈现其他所有内容的根组件。
他们描述的外观,但不知道数据来自哪里,或者如何改变它。他们只渲染给他们的东西。如果你从Redux迁移到其他的东西,你将能够保持所有这些组件完全一样。他们没有依赖Redux。
设计容器组件
我们还需要一些容器组件将演示组件连接到Redux。例如,演示TodoList组件需要一个容器,如VisibleTodoList,该容器订阅Redux存储并知道如何应用当前可见性过滤器。要更改可见性过滤器,我们将提供一个FilterLink容器组件,用于呈现链接,并在点击时分配适当的操作:
- VisibleTodoList根据当前可见性过滤器过滤待办事项并呈现TodoList。
- FilterLink获取当前可见性过滤器并呈现链接。
- filter: string是它代表的可见性过滤器。
设计其他组件
有时很难判断某个组件应该是一个表示组件还是一个容器。例如,有时窗体和函数实际上是耦合在一起的,比如在这个小部件的情况下:
- AddTodo是一个带有“添加”按钮的输入字段
从技术上讲,我们可以将它分成两个部分,但现阶段可能为时过早。在非常小的组件中混合表示和逻辑是很好的。随着它的增长,如何分割它将会更加明显,所以我们会把它混合起来。
实现组件
我们来编写组件!我们从演示组件开始,所以我们不需要考虑绑定到Redux。
实现演示组件
这些都是普通的React组件,所以我们不会详细检查它们。我们编写功能无状态的组件,除非我们需要使用本地状态或生命周期方法。这并不意味着表示组件必须是功能 - 这样更容易定义它们。如果您需要添加本地状态,生命周期方法或性能优化,则可以将它们转换为类。components/Todo.js
123456789101112131415161718192021 | import React from 'react'import PropTypes from 'prop-types'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}export default Todo |
components/TodoList.js
123456789101112131415161718192021222324 | import React from 'react'import PropTypes from 'prop-types'import Todo from './Todo'const TodoList = ({ todos, onTodoClick }) => ( <ul> {todos.map((todo, index) => ( <Todo key={index} {...todo} onClick={() => onTodoClick(index)} /> ))} </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 |
components/Link.js
1234567891011121314151617181920212223242526272829 | importimport ReactReact fromfrom ''reactreact'' importimport PropTypesPropTypes fromfrom ''prop-typesprop-types'' constconst LinkLink == ({ active, children, onClick }) ({ active, childr => { if (active) { return <span>{children}</span> } return ( <a href="" onClick={e => { e.preventDefault() onClick() }} > {children} </a> )}Link.propTypes = { active: PropTypes.bool.isRequired, children: PropTypes.node.isRequired, onClick: PropTypes.func.isRequired}export default Link |
components/Footer.js
123456789101112131415161718192021222324 | importimport ReactReact fromfrom ''reactreact'' importimport FilterLinkFilterLink fromfrom ''../containers/FilterLink../containers/Filter 'import { VisibilityFilters } from '../actions'const Footer = () => ( <p> Show: {' '} <FilterLink filter={VisibilityFilters.SHOW_ALL}> All </FilterLink> {', '} <FilterLink filter={VisibilityFilters.SHOW_ACTIVE}> Active </FilterLink> {', '} <FilterLink filter={VisibilityFilters.SHOW_COMPLETED}> Completed </FilterLink> </p>)export default Footer |
实现容器组件
现在是时候通过创建一些容器将这些表示性组件连接到Redux。从技术上讲,容器组件只是一个React组件,它使用store.subscribe()来读取Redux状态树的一部分,并为它呈现的呈现组件提供props。您可以手动编写容器组件,但我们建议使用React Redux库的connect()函数生成容器组件,该函数提供许多有用的优化以防止不必要的重新呈现。(这样做的一个结果是你不必担心自己实现shouldComponentUpdate的React性能建议。)
要使用connect(),需要定义一个名为mapStateToProps的特殊函数,它告诉如何将当前的Redux存储状态转换为要传递给要包装的表示组件的道具。例如,VisibleTodoList需要计算待办事项以传递给TodoList,所以我们定义一个函数,根据state.visibilityFilter过滤state.todos,并在其mapStateToProps中使用它:
1234567891011121314151617 | const getVisibleTodos = (todos, filter) => { switch (filter) { case 'SHOW_COMPLETED': return todos.filter(t => t.completed) case 'SHOW_ACTIVE': return todos.filter(t => !t.completed) case 'SHOW_ALL': default: return todos }}const mapStateToProps = state => { return { todos: getVisibleTodos(state.todos, state.visibilityFilter) }} |
除了读取状态之外,容器组件还可以调度操作。以类似的方式,您可以定义一个名为mapDispatchToProps()的函数,它接收dispatch()方法并返回要注入演示组件的回调支持。例如,我们希望VisibleTodoList将名为onTodoClick的道具注入TodoList组件,并且我们希望onTodoClick发送TOGGLE_TODO操作:
1234567 | const mapDispatchToProps = dispatch => { return { onTodoClick: id => { dispatch(toggleTodo(id)) } }} |
最后,我们通过调用connect()并传递这两个函数来创建VisibleTodoList:
12345678 | import { connect } from 'react-redux'const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps)(TodoList)export default VisibleTodoList |
这些是React Redux API的基础知识,但有几个捷径和开关选项,因此我们鼓励您详细查看其文档。如果您担心mapStateToProps过于频繁地创建新对象,则可能需要了解使用重新选择计算派生数据。
查找下面定义的其余容器组件:container/FilterLink.js
123456789101112131415161718192021222324 | import { connect } from 'react-redux'import { setVisibilityFilter } from '../actions'import Link from '../components/Link'const mapStateToProps = (state, ownProps) => { return { active: ownProps.filter === state.visibilityFilter }}const mapDispatchToProps = (dispatch, ownProps) => { return { onClick: () => { dispatch(setVisibilityFilter(ownProps.filter)) } }}const FilterLink = connect( mapStateToProps, mapDispatchToProps)(Link)export default FilterLink |
containers/VisibleTodoList.js
1234567891011121314151617181920212223242526272829303132333435 | 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 |
实现其他组件
containers/AddTodo.js
如前所述,AddTodo组件的展示和逻辑都被混合到一个单一的定义中。
12345678910111213141516171819202122232425262728293031323334 | import React from 'react'import { connect } from 'react-redux'import { addTodo } from '../actions'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)export default AddTodo |
如果您不熟悉ref属性,请阅读本文档以熟悉推荐使用此属性。
将容器捆绑在一个组件内
components/App.js
1234567891011121314 | import React from 'react'import Footer from './Footer'import AddTodo from '../containers/AddTodo'import VisibleTodoList from '../containers/VisibleTodoList'const App = () => ( <div> <AddTodo /> <VisibleTodoList /> <Footer /> </div>)export default App |
传递Store
所有容器组件都需要访问Redux存储,以便他们可以订阅它。一种选择是将其作为props传递给每个容器组件。然而它很乏味,因为即使是通过表示组件,也只是因为它们碰巧在组件树中渲染容器,所以你必须连线存储。
我们推荐的选项是使用名为index.js
123456789101112131415 | import React from 'react'import { render } from 'react-dom'import { Provider } from 'react-redux'import { createStore } from 'redux'import todoApp from './reducers'import App from './components/App'const store = createStore(todoApp)render( <Provider store={store}> <App /> </Provider>, document.getElementById('root')) |