本教程是 Brad Westfall 三部分系列教程的最后一部分。我们将学习如何在整个应用程序中高效地管理状态,并且以一种可扩展的方式避免危险的复杂性。在我们的 React 之旅中,我们已经走了很远,值得跨越终点线,从这种开发方法中获得最大的收益。
Redux 是一个用于管理 JavaScript 应用程序中的数据状态和 UI 状态的工具。它非常适合单页面应用程序 (SPA),在 SPA 中,随着时间的推移管理状态可能会变得很复杂。它也是与框架无关的,因此尽管它是专为 React 而设计的,但它甚至可以 与 Angular 或 jQuery 应用程序一起使用。
此外,它是在一个“时间旅行”实验中诞生的——千真万确,我们稍后会谈到这一点!
正如我们在之前的教程中所见,React 通过组件“流动”数据。更准确地说,这被称为“单向数据流”——数据从父级流向子级。由于这种特性,两个非父子组件在 React 中如何通信并不明显。
React 不推荐这种直接的组件到组件通信。即使它确实有支持这种方法的功能,许多人认为这是一种不好的做法,因为直接的组件到组件通信容易出错,会导致 意大利面条代码——一个古老的术语,指的是难以理解的代码。
React 提供了一个建议,但他们希望你自己实现它。以下是 React 文档 中的一节内容。
对于两个没有父子关系的组件之间的通信,可以设置自己的全局事件系统。… Flux 模式是安排此任务的一种可能方法。
这就是 Redux 派上用场的地方。Redux 提供了一种解决方案,可以将所有应用程序状态存储在一个名为“存储”的地方。然后,组件将状态更改“分派”到存储,而不是直接分派到其他组件。需要了解状态更改的组件可以“订阅”到存储。
存储可以被认为是应用程序中所有状态更改的“中间人”。在引入 Redux 后,组件不再直接相互通信,而是所有状态更改都必须通过单一事实来源,即存储。
这与 其他策略 大不相同,在其他策略中,应用程序的各个部分彼此直接通信。有时,这些策略被认为容易出错,并且难以理解。
使用 Redux,所有组件都从存储中获取状态,这一点很清楚。同样,组件应该将状态更改发送到哪里也很清楚——同样是存储。启动更改的组件只需要关心将更改分派到存储,而不必担心需要状态更改的其他组件列表。这就是 Redux 如何使数据流更容易理解。
使用存储来协调应用程序状态的一般概念被称为 Flux 模式。它是一种设计模式,补充了 React 之类的单向数据流架构。Redux 与 Flux 类似,但它们到底有多相似呢?
Redux 是“类 Flux”的
Flux 是一种模式,而不是像 Redux 这样的工具,因此它不是你可以下载的东西。Redux 是一种受 Flux 模式启发的工具,以及其他一些东西,比如 Elm。有很多指南比较 Redux 和 Flux。大多数指南都会得出结论,Redux是 Flux 或者是类 Flux 的,具体取决于对 Flux 规则的严格程度。最终,这并不重要。Facebook 喜欢并非常支持 Redux,因此他们 聘用了 Redux 的主要开发者 Dan Abramov。
本文假设你完全不熟悉 Flux 模式。但如果你熟悉,你会注意到一些细微的差异,尤其是考虑到 Redux 的 三大指导原则。
1. 单一事实来源
Redux 只使用一个存储来保存所有应用程序状态。由于所有状态都驻留在一个地方,因此 Redux 将其称为单一事实来源。
存储的数据结构最终由你决定,但对于实际的应用程序来说,它通常是一个深度嵌套的对象。
Redux 的这种单存储方法是它与 Flux 的多存储方法之间的主要差异之一。
2. 状态是只读的
根据 Redux 文档,“修改状态的唯一方法是发出一个 action,即一个描述发生了什么的对象。”
这意味着应用程序不能直接修改状态。相反,“action”被分派以表达更改存储中状态的意图。
存储对象本身的 API 非常小,只有四个方法。
store.dispatch(action)
store.subscribe(listener)
store.getState()
replaceReducer(nextReducer)
因此,正如你所看到的,没有设置状态的方法。因此,分派 action 是应用程序代码表达状态更改的唯一方法。
var action = {
type: 'ADD_USER',
user: {name: 'Dan'}
};
// Assuming a store object has been created already
store.dispatch(action);
dispatch()
方法将一个对象发送到 Redux,称为 action。action 可以被描述为一个“有效负载”,它包含一个type
和所有可用于更新状态的其他数据——在本例中为用户。请记住,在type
属性之后,action 对象的设计由你决定。
3. 使用纯函数进行更改
正如前面所述,Redux 不允许应用程序直接更改状态。相反,分派的 action “描述”了状态更改以及更改状态的意图。reducer 是你编写的函数,它们处理分派的 action 并可以实际更改状态。
reducer 将当前状态作为参数传入,并且只能通过返回新状态来修改状态。
// Reducer Function
var someReducer = function(state, action) {
...
return state;
}
reducer 应该被写成“纯”函数,这是一个描述具有以下特征的函数的术语。
- 它不会进行外部网络或数据库调用。
- 它的返回值完全取决于其参数的值。
- 它的参数应该被认为是“不可变的”,这意味着它们不应该被更改。
- 使用相同的参数集调用纯函数将始终返回相同的值。
这些函数被称为“纯”函数,因为它们除了根据参数返回值之外什么都不做。它们对系统的任何其他部分都没有副作用。
我们的第一个 Redux 存储
首先,使用 Redux.createStore()
创建一个存储,并将所有 reducer 作为参数传入。让我们看一个只有一个 reducer 的简单示例。
// Note that using .push() in this way isn't the
// best approach. It's just the easiest to show
// for this example. We'll explain why in the next section.
// The Reducer Function
var userReducer = function(state, action) {
if (state === undefined) {
state = [];
}
if (action.type === 'ADD_USER') {
state.push(action.user);
}
return state;
}
// Create a store by passing in the reducer
var store = Redux.createStore(userReducer);
// Dispatch our first action to express an intent to change the state
store.dispatch({
type: 'ADD_USER',
user: {name: 'Dan'}
});
以下是发生的事情的简要总结。
- 使用一个 reducer 创建存储。
- reducer 确定应用程序的初始状态是一个空数组。*
- 使用 action 中的新用户进行分派。
- reducer 将新用户添加到状态并返回它,从而更新存储。
* 在示例中,reducer 实际上被调用了两次——一次是在创建存储时,另一次是在分派之后。
当创建存储时,Redux 会立即调用 reducer 并使用它们的返回值作为初始状态。对 reducer 的第一次调用会将undefined
作为状态传入。reducer 代码会预测这种情况,并返回一个空数组来启动存储的初始状态。
Reducers 也在每次调度 action 时被调用。由于 reducer 返回的状态将成为 store 中的新状态,Redux 始终期望 reducers 返回状态。
在这个例子中,对 reducer 的第二次调用是在调度之后。请记住,一个调度的 action 描述了改变状态的意图,并且通常会携带新状态的数据。这一次,Redux 将当前状态(仍然是一个空数组)以及 action 对象传递给 reducer。action 对象现在具有类型属性 'ADD_USER'
,它允许 reducer 知道如何改变状态。
很容易将 reducers 想象成漏斗,允许状态通过它们。这是因为 reducers 始终接收和返回状态以更新 store
根据这个例子,我们的 store 现在将成为一个包含一个用户对象的数组。
store.getState(); // => [{name: 'Dan'}]
不要修改状态,复制它
虽然我们示例中的 reducer 在技术上有效,但它会修改状态,这是不好的做法。即使 reducers 负责改变状态,它们也永远不应该直接修改“当前状态”参数。这就是为什么我们不应该在 reducer 的状态参数上使用 .push()
,一个 修改方法。
传递给 reducer 的参数应该被视为不可变的。换句话说,它们不应该被直接更改。我们可以使用 非修改方法,如 .concat()
,来实质上创建一个数组的副本,然后我们可以更改并返回这个副本。
var userReducer = function(state = [], action) {
if (action.type === 'ADD_USER') {
var newState = state.concat([action.user]);
return newState;
}
return state;
}
通过对 reducer 的这个更新,添加一个新用户会导致状态参数的副本被更改并返回。当不添加新用户时,请注意返回的是原始状态,而不是创建一个副本。
下面有一整节内容是关于不可变数据结构,它将更深入地阐述这些最佳实践。
多个 Reducers
最后一个例子是一个很好的入门,但大多数应用程序将需要更复杂的状态来表示整个应用程序。由于 Redux 只使用一个 store,我们将需要使用嵌套对象将状态组织成不同的部分。让我们想象一下,我们希望我们的 store 类似于这个对象。
{
userState: { ... },
widgetState: { ... }
}
它仍然是“一个 store = 一个对象”用于整个应用程序,但它具有 userState
和 widgetState
的嵌套对象,它们可以包含各种数据。这可能看起来过于简单,但它实际上并不像真正的 Redux store 那样复杂。
为了创建一个包含嵌套对象的 store,我们需要用 reducer 来定义每个部分。
import { createStore, combineReducers } from 'redux';
// The User Reducer
const userReducer = function(state = {}, action) {
return state;
}
// The Widget Reducer
const widgetReducer = function(state = {}, action) {
return state;
}
// Combine Reducers
const reducers = combineReducers({
userState: userReducer,
widgetState: widgetReducer
});
const store = createStore(reducers);
combineReducers()
的使用允许我们根据不同的逻辑部分来描述我们的 store,并将 reducers 分配给每个部分。现在,当每个 reducer 返回初始状态时,该状态将进入 store 的相应 userState
或 widgetState
部分。
需要注意的非常重要的一点是,现在每个 reducer 都被传递了其相应的部分状态,而不是像单个 reducer 示例中那样传递整个 store 的状态。然后,每个 reducer 返回的状态将应用于其相应的部分。
调度后调用哪个 Reducer?
所有 reducer。当我们将 reducers 比作漏斗时,更明显的是,每次调度 action 时,所有 reducers 都将被调用,并且将有机会更新它们相应的状态。
我谨慎地说“它们”的状态,因为 reducer 的“当前状态”参数及其返回的“更新”状态只影响 store 中该 reducer 的部分。请记住,如上一节所述,每个 reducer 只传递了其相应的状态,而不是整个状态。
Action 策略
实际上,创建和管理 action 和 action 类型的策略有很多。虽然了解它们非常重要,但它们不像本文中的一些其他信息那样重要。为了使文章更简洁,我们已经记录了您应该了解的基本 action 策略 在与本系列相关的 GitHub 存储库中。
不可变数据结构
状态的形状由您决定:它可以是基本类型、数组、对象,甚至是一个 Immutable.js 数据结构。唯一重要的是您不应该修改状态对象,如果状态发生变化,则返回一个新的对象。”— Redux 文档
该语句说得很多,我们已经在本教程中提到了这一点。如果我们要开始讨论不可变和可变的来龙去脉和优缺点,我们可以用一整篇 博客文章的内容来阐述。因此,我只会突出一些主要要点。
首先
- JavaScript 的基本数据类型(Number、String、Boolean、Undefined 和 Null)已经是不可变的。
- 对象、数组和函数是可变的。
据说数据结构上的可变性容易导致错误。由于我们的 store 将由状态对象和数组组成,我们需要实现一种策略来保持状态不可变。
让我们想象一个 state
对象,我们需要更改其中的一个属性。以下是三种方法。
// Example One
state.foo = '123';
// Example Two
Object.assign(state, { foo: 123 });
// Example Three
var newState = Object.assign({}, state, { foo: 123 });
第一个和第二个示例修改了状态对象。第二个示例修改了状态,因为 Object.assign()
将所有参数合并到第一个参数中。但这也是第三个示例不修改状态的原因。
第三个示例将 state
和 {foo: 123}
的内容合并到一个全新的空对象中。这是一个常见的技巧,它允许我们实质上创建一个状态的副本,并在不影响原始 state
的情况下修改副本。
对象“扩展运算符”是保持状态不可变的另一种方法。
const newState = { ...state, foo: 123 };
有关正在发生的事情和它对 Redux 的好处的详细解释,请参阅 他们在该主题上的文档。
Object.assign()
和扩展运算符都是 ES2015。
总之,有许多方法可以明确地保持对象和数组不可变。许多开发者使用像 seamless-immutable、Mori 甚至 Facebook 自己的 Immutable.js 这样的库。
初始状态和时间旅行
如果您 阅读文档,您可能会注意到 createStore()
的第二个参数,用于“初始状态”。这可能看起来像是 reducers 创建初始状态的另一种方法。但是,此初始状态只应用于“状态水化”。
想象一下,用户刷新了您的 SPA,并且 store 的状态被重置为 reducer 的初始状态。这可能不是您想要的。
相反,想象一下,您可以使用一种策略来持久化 store,然后您可以在刷新时将其重新水化到 Redux 中。这就是将初始状态发送到 createStore()
的原因。
但这引出了一个有趣的概念。如果重新水化旧状态如此简单方便,您可以想象在您的应用程序中进行状态“时间旅行”。这对于调试或甚至 撤消/重做功能 非常有用。将所有状态存储在一个 store 中对于这些以及许多其他原因都非常有意义!这仅仅是不可变状态对我们有帮助的原因之一。
在一次采访中,Dan Abramov 被问到 “为什么您开发了 Redux?”
我并没有打算创建一个 Flux 框架。当 React Europe 首次宣布时,我提议了一个关于“热重载和时间旅行”的演讲,但说实话,我不知道如何实现时间旅行。
Redux 与 React
正如我们已经讨论过的,Redux 是与框架无关的。在考虑 Redux 如何与 React 一起工作之前,首先了解 Redux 的核心概念很重要。但现在我们已经准备好从上一篇文章中获取一个容器组件,并将 Redux 应用于它。
首先,这是没有 Redux 的原始组件
import React from 'react';
import axios from 'axios';
import UserList from '../views/list-user';
const UserListContainer = React.createClass({
getInitialState: function() {
return {
users: []
};
},
componentDidMount: function() {
axios.get('/path/to/user-api').then(response => {
this.setState({users: response.data});
});
},
render: function() {
return <UserList users={this.state.users} />;
}
});
export default UserListContainer;
当然,它执行了它的 Ajax 请求并更新了自己的本地状态。但如果应用程序中的其他区域需要根据新获取的用户列表进行更改,这种策略就无法满足。
使用 Redux 策略,我们可以当 Ajax 请求返回时调度一个动作,而不是执行 this.setState()
。然后,这个组件和其他组件可以订阅状态更改。但这实际上引出了一个问题,我们如何设置 store.subscribe()
来更新组件的状态?
我想我可以提供一些手动将组件连接到 Redux 存储的示例。你甚至可能想象到如何用自己的方法来实现。但最终,在这些例子之后,我会解释说有一种更好的方法,并且忘记手动例子。然后我会介绍官方的 React/Redux 绑定模块,名为react-redux。所以让我们直接跳到那里。
react-redux
连接
使用 为了清楚起见,react
、redux
和 react-redux
是 npm 上的三个独立模块。react-redux
模块允许我们以更方便的方式将 React 组件“连接”到 Redux。
它看起来像这样
import React from 'react';
import { connect } from 'react-redux';
import store from '../path/to/store';
import axios from 'axios';
import UserList from '../views/list-user';
const UserListContainer = React.createClass({
componentDidMount: function() {
axios.get('/path/to/user-api').then(response => {
store.dispatch({
type: 'USER_LIST_SUCCESS',
users: response.data
});
});
},
render: function() {
return <UserList users={this.props.users} />;
}
});
const mapStateToProps = function(store) {
return {
users: store.userState.users
};
}
export default connect(mapStateToProps)(UserListContainer);
有很多新东西正在发生
- 我们从
react-redux
中导入了connect
函数。 - 这段代码从下往上看可能更容易理解,从连接开始。
connect()
函数实际上接受两个参数,但我们只显示了mapStateToProps()
的一个参数。看到
connect()()
的额外一组括号可能看起来很奇怪。这实际上是两个函数调用。第一个调用connect()
返回另一个函数。我想我们可以将该函数分配给一个名称,然后调用它,但为什么要这样做呢?我们直接使用第二组括号调用它就行了。此外,在该函数被调用之后,我们不需要该第二个函数名存在于任何原因。但是,第二个函数需要你传递一个 React 组件。在本例中,它是我们的容器组件。如果你在想“为什么要把它做得比必须的更复杂呢?”,我可以理解。但这实际上是一种常见的“函数式编程”范式,所以学习它很好。
connect()
的第一个参数是一个应该返回对象的函数。对象的属性将成为组件的“props”。你可以看到它们的值来自状态。现在,我希望函数名“mapStateToProps”更有意义。还要注意,mapStateToProps()
将接收一个参数,即整个 Redux 存储。mapStateToProps()
的主要思想是隔离该组件需要作为其 props 的整体状态的哪些部分。- 由于 #3 中提到的原因,我们不再需要
getInitialState()
存在。还要注意,我们引用的是this.props.users
而不是this.state.users
,因为users
数组现在是一个 prop,而不是本地组件状态。 - Ajax 返回现在调度一个动作,而不是更新本地组件状态。为了简洁起见,我们不使用动作创建者或动作类型常量。
代码示例假设了用户 reducer 的工作方式,这可能并不明显。请注意存储如何具有 userState
属性。但这个名字从哪里来的呢?
const mapStateToProps = function(store) {
return {
users: store.userState.users
};
}
这个名字来自于我们组合 reducer 时。
const reducers = combineReducers({
userState: userReducer,
widgetState: widgetReducer
});
那么 userState
的 .users
属性呢?它从哪里来的呢?
虽然我们没有在示例中展示实际的 reducer(因为它将位于另一个文件中),但它是决定其各自状态的子属性的 reducer。为了确保 .users
是 userState
的一个属性,这些示例的 reducer 可能看起来像这样
const initialUserState = {
users: []
}
const userReducer = function(state = initialUserState, action) {
switch(action.type) {
case 'USER_LIST_SUCCESS':
return Object.assign({}, state, { users: action.users });
}
return state;
}
Ajax 生命周期调度
在我们的 Ajax 示例中,我们只调度了一个动作。它被有意地称为 'USER_LIST_SUCCESS'
,因为我们可能还想在 Ajax 开始之前调度 'USER_LIST_REQUEST'
,并在 Ajax 失败时调度 'USER_LIST_FAILED'
。请务必阅读关于异步动作的文档。
从事件中调度
在上一篇文章中,我们看到事件应该从容器传递到演示组件。事实证明,react-redux
在事件只需要调度一个动作的情况下也有帮助。
...
const mapDispatchToProps = function(dispatch, ownProps) {
return {
toggleActive: function() {
dispatch({ ... });
}
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(UserListContainer);
在演示组件中,我们可以执行 onClick={this.props.toggleActive}
,就像我们之前做过的那样,但这次我们不需要自己编写事件。
容器组件省略
有时,容器组件只需要订阅存储,并且不需要任何方法,例如 componentDidMount()
来启动 Ajax 请求。它可能只需要一个 render()
方法来将状态传递到演示组件。在这种情况下,我们可以用这种方式创建一个容器组件
import React from 'react';
import { connect } from 'react-redux';
import UserList from '../views/list-user';
const mapStateToProps = function(store) {
return {
users: store.userState.users
};
}
export default connect(mapStateToProps)(UserList);
是的,朋友们,这就是我们新的容器组件的整个文件。但是等等,容器组件在哪里?为什么我们这里没有使用 React.createClass()
呢?
事实证明,connect()
为我们创建了一个容器组件。请注意,这次我们直接传递了演示组件,而不是创建自己的容器组件来传递。如果你真的考虑了容器组件的作用,记住它们的存在是为了让演示组件只关注视图,而不是状态。它们还将状态作为 props 传递到子视图中。而这正是 connect()
所做的——它将状态(通过 props)传递到我们的演示组件中,并且实际上返回一个包装演示组件的 React 组件。本质上,该包装器就是一个容器组件。
那么,是否意味着之前的示例实际上是两个包装了演示组件的容器组件呢?当然,你可以这样想。但这并不成问题,它只在我们的容器组件需要除了 render()
之外的更多 React 方法时才必要。
可以将这两个容器组件视为扮演着不同但相关的角色
嗯,也许这就是 React 徽标看起来像原子的原因!
Provider
为了让任何 react-redux
代码都能正常工作,你需要让你的应用程序知道如何使用 react-redux
和 <Provider />
组件。这个组件包装了你的整个 React 应用程序。如果你使用的是 React Router,它看起来像这样
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import router from './router';
ReactDOM.render(
<Provider store={store}>{router}</Provider>,
document.getElementById('root')
);
附加到 Provider 的 store
是真正通过 react-redux
将 React 和 Redux “连接”起来的。这个文件是一个主要入口点可能是什么样子的示例。
Redux 与 React Router
这不是必需的,但还有一个名为react-router-redux 的 npm 项目。由于路由从技术上讲是 UI 状态的一部分,而 React Router 不了解 Redux,因此该项目有助于将两者连接起来。
你看到我做了什么吗?我们又回到了起点,回到了第一篇文章!
最终项目
本系列的最终项目指南 将帮助您创建一个小型“用户和部件”单页应用程序。

与本系列中的其他文章一样,每篇文章都附带一个指南,该指南在 GitHub 上提供了更多关于指南工作原理的文档。
摘要
我真的希望您像我写这篇文章一样享受这篇文章。我知道还有很多关于 React 的主题我们没有涵盖(例如表单),但我试图保持我对这个前提的忠诚,即我想让 React 的新用户了解如何超越基础知识,以及创建单页应用程序的感觉。
虽然很多人提供了帮助,但要特别感谢Lynn Fisher为本教程提供的精美图形!
优秀的文章。
很棒的文章!
顺便说一句,发现了一个拼写错误。
那只是为了看看你是否注意到了。开玩笑,谢谢 Jeremy
明白了!
感谢您撰写本系列文章。我最近开始学习各种与 NodeJS 相关的技术。在阅读了许多教程之后,我可以断言,您的文章是我见过的最有帮助的介绍。再次感谢!
哇!这真是一个 CSSfuckingtrick!
这些文章非常棒,感谢 Brad!
又是一篇有趣的文章,但我一点也不喜欢 Redux。使用包含操作类型标识符的字符串常量,并使用 switch-case 语句来处理实际逻辑,这让我感觉像是 C 或 C++,而不是惯用的 JavaScript。当 JavaScript 看起来像 C 或 C++ 时,就说明有些地方不对劲,哈哈。我非常希望看到一个看起来更像 JavaScript 的示例 :)
我并不喜欢字符串常量策略,但这是我见过的最常见的策略,所以我认为我会继续使用它。我认为它教会了我们正在尝试做的事情的基本原理。我非常喜欢
redux-act
,它是一种不同的方法。这是一个项目,它可以减轻操作类型和操作创建者带来的样板代码的痛苦。它不使用字符串常量方法,事实上它将整个过程从您那里抽象出来,这很好。我不会因此责怪 Redux。这只是众多策略之一,可以使用,与 Redux 提供的功能关系不大。主要思想是,我需要调度一些东西,并且让正确的 reducer 了解它 - 任何你能想到的策略,只要能做到这一点就可以了。太棒了!我的大脑要处理很多东西,但我将开始一个测试项目并尝试应用这些概念。感谢您花时间写出这样一篇好文章!
感谢 Victor,还有 Mike、Balazs 和 Darrly
您好。为什么要在 #我们的第一个 Redux 存储 中使用关键字 VAR 来调用 // 调度我们的第一个操作以表示更改状态的意图?
var store.dispatch({
type: ‘ADD_USER’,
user: {name: ‘Dan’}
});
您好 Tim,很抱歉,我不明白您的问题。
抱歉我的英语不好。
在为 #我们的第一个 Redux 存储 部分编写的示例代码中
在结尾处写着
var store.dispatch({
type: ‘ADD_USER’,
user: {name: ‘Dan’}
});
所以我只是想弄清楚为什么“var”在调用函数 store.dispatch 之前存在。
我不小心创建了一个全新的评论,而不是回复。请参阅下方。
哦!那是一个拼写错误,我将看看 Chris 是否可以删除它。我可以理解为什么这会让人困惑,这是不对的。您是第一个指出这一点的人,在所有审阅过它的人中 :)
哦,谢谢。现在我明白了)
实际上,我对 Redux 非常困惑。在第二张显示组件树的图片中。如何从最底层的哑组件调用或启动更改存储,而不会将其传递到整个树中?或者最好不要这样做?如果是这样,为什么?
我会说“展示性”而不是“哑”,因为这基本上是行业想要指代它的方式。但假设展示性组件想要启动更改。通常,需要更改状态是因为某个事件。因此,根据上一篇文章,请记住我们可以将事件从容器组件传递到展示性组件。因此,虽然展示性组件实际上是处理
onClick
,但它正在调用一个位于容器组件中的函数。容器负责处理状态,因此由onClick
(在容器中)调用的函数正在执行store.dispatch(...)
。GitHub 上的演示代码展示了这一点。这是一个处理点击的展示性“视图”组件 示例在此处
这是它的容器组件,其中
deleteUser
引用一个函数,该函数是一个 API 调用:示例在此处这个容器组件可以这样写
不过在我的代码中我没有这样做,因为我想在调度之前执行一个 XHR 调用,这就是为什么实际代码调用
userApi.deleteUser
,这样它就可以执行 XHR 然后在成功返回时调度。据我所知,执行 XHR 和存储调度的策略有很多种。这只是一个简单的方法。但希望代码能为您提供更多上下文,而文章只粗略地提到了这一点。我不明白为什么需要将展示性组件包装在容器中。可以请您再解释一下吗?
还有一个问题
例如,我有一个包含 10 个级别的巨大组件树,例如,只有最后一个组件中的操作按钮才显示状态。在这种情况下,我需要从我连接到存储的树顶向下传递一个事件操作,还是怎样?非常感谢您在这方面给予我的帮助。您关于 React 的文章对我很重要。
我可以假设您阅读了关于容器组件 的上一篇文章吗?前提是我们希望展示性组件(视图)不知道状态。这使得它们更具可重用性。想象一下,展示性视图可能是一个确认提示。如果它是无状态的,并且只从容器接收状态,那么它可以与多个容器组件一起使用,用于不同的状态上下文。因此,作为一种良好的做法,我们始终希望保持展示性组件无状态。
关于您的第二个问题,如果我理解正确,您可能有 10 个被告,就像您提到的那样,最底层的被告捕获了实际事件,而最上层的父组件是控制状态的容器 - 那么我想您可以沿袭向下传递事件处理程序。在这种情况下,请务必查看扩展属性,它们可以轻松地将道具从父组件传递到子组件,再从子组件传递到子组件,等等 - 或者 - 我不确定为什么您有 10 个嵌套组件,但只需知道,一般来说,大多数展示性视图组件都有一个与之配套的容器。因此,您不必只在该长后代视图列表的顶部有一个。您可以有一个容器包含一个视图,然后该视图有一个子组件,该子组件是一个容器,包含一个视图。因此,对于我的 React 编写风格来说,拥有大量的嵌套视图而没有容器组件在其中,这很不寻常。然后请记住,任何容器组件都可以与状态通信。我真的希望这有意义 :)
我认为你可能已经到了需要考虑架构问题的阶段,而这些问题可能无法在文章中直接找到答案。当我处于这种状态时 - 现在也是如此 - 我发现浏览 GitHub 上大量示例代码或 React 项目模板很有帮助,可以了解它们的实现方式。我建议你一定要查看我为这篇文章发布的 GitHub 代码。
哦,可重用性!当然!这解释了很多!
但第二,在这种情况下,没有 Redux 更好,我可以将所有状态都保存在一个主容器中,仅此而已,应用程序在纯粹的 React 中会更快。可能我还没有准备好使用 Redux。
感谢你的文章,对我帮助很大。尤其是关于组件的部分。
嗨,Brad…很棒的文章。请问你使用什么工具创建这些图表?谢谢。
我的一个朋友帮我做的。她在总结部分结尾处获得了署名。
这是我迄今为止读过的关于这个主题的最好的文章之一!解释得非常清楚,谢谢!
这可能是学习 React 的最佳文章。感谢你撰写了这些很棒的文章。
我完全同意,我已经阅读了几天,试图弄清楚 Redux 的概念。这个解释简单明了,切中要害。现在一切都说得通了,也让我真正爱上了 Redux。
嗨,Brad,
感谢你撰写了这些很棒的系列文章。
我有一个问题,如果我构建一个没有 Redux 的网站,可能会遇到哪些问题?
Redux 是一个必须使用的工具吗?
我需要尽快完成这个网站。提前感谢!
嗨,Brad,
首先,很棒的文章。这是我唯一找到 Reactjs 和 React-router 更大型应用示例的地方。
我正在使用前两个教程中的 React 和 React-router。
我的网站也需要使用 Redux 吗?
我需要尽快构建一个 SPA,所以如果我不使用 Redux,可能会遇到哪些问题?
提前感谢!
Redux 当然不是 React 应用程序的必要组件,但任何规模的应用程序都需要某种形式的状态管理。使用本地状态管理(React 为每个组件内置的状态功能)很好,但对我来说,我发现对更大的应用程序使用 Redux 更好,因为所有组件都可以共享状态。如果你没有使用 Redux(或类似的东西),你如何在应用程序范围内管理状态?
非常感谢你,Brad,
很棒的文章,很棒的图片,很棒的 CSS…一切都很棒。
但我希望再深入一点。
只是想问一下,是否会有一篇关于将你的内容移植到 MDL(Material Design Lite)(这个网站是css-tricks!)或其他 Material Design CSS 框架上的文章。
我自己的工作试图将这篇文章和 react-router 与 MDL 框架合并,让我非常沮丧。
我已经像 index 上的示例一样设置了
index.html
,并在main-layout.js
中翻译了一些示例,比如 这个,但我不知道如何将菜单与 react-router 的Link
组件链接起来,并在“容器页面”中显示结果。提前感谢
嗨,Santi,实际上我对 React 和 Material Design 没有什么经验。我的这个系列文章在这个最后一篇文章中已经基本结束了。但我不知道 Chris 能否找到人来写一篇关于你正在谈论内容的文章。抱歉我帮不了更多。
这篇博文被收录在 The React Newsletter #25 中。 http://eepurl.com/bWc4Wf