我在大多数Redux开发人员中发现的一个普遍趋势是对setState()
的仇恨。 我们中的许多人(是的,我之前多次陷入这个陷阱)在setState()
面前退缩,并尝试将所有数据保留在我们的Redux存储中。 但是,随着应用程序复杂性的增加,这带来了一些挑战。
在本文中,我将引导您完成各种用于建模状态的策略,并深入探讨何时可以使用它们。
入门
Redux的原则是成为应用程序状态的唯一事实来源。 新的《权力的游戏》(Game of Thrones)季节即将播出,我敢肯定,每个人都很高兴知道这种情况将如何发展。 让我们构建一个有趣的《权力的游戏》粉丝列表页面,以详细了解这些概念。
注意:我将使用yarn
运行该应用程序。 如果没有设置纱线,则将纱线替换为npm
。
之前,我们在跳水,从下载的基本骨架回购和运行:
yarn install
yarn run start
您应该看到一个基本列表页面,其中列出了一些您喜欢的GoT字符。
注意:我们将使用ducks模式编写应用程序。 它减少了不必要的模块导入,并减少了许多样板。
Redux简介
本文的范围是为了帮助您构建Redux应用程序。 它假定了图书馆的基本知识。 我将简要介绍Redux概念,以帮助您更好地遵循本文的其余部分。 如果您熟悉这些方法的工作原理,请随时跳过本节。
所有Redux应用程序都使用四个重要的结构:动作,reduce,商店和容器。
动作
动作是为了更新状态。 它可能是由网络呼叫或用户单击按钮触发的。 动作分为两个部分:
- 动作类型 。 代表动作的唯一标识符。
- 有效载荷 。 与操作关联的任何元数据。 例如,如果我们发出网络请求以获取电影列表,则服务器的响应就是有效负载。
在此示例中,我们将使用称为redux-actions
的库来创建操作。
减速器
减速器是侦听动作并返回新状态表示的函数。
商店
一个应用程序可以分为许多化简,代表页面的各个部分。 商店将所有这些结合在一起,并保持应用程序状态不变。
货柜
容器将您的应用程序状态和操作与组件连接起来,将它们作为道具传递下来。
为了深入了解它是如何工作的,我鼓励您首先看一下Dan Abramov的免费介绍系列 。
拆分应用数据和UI状态
列表页面很好,但是名称并没有给GoT领域新手提供任何上下文。 让我们扩展该组件以呈现字符描述:
//GoTCharacter.js
export const CharacterRow = ({character}) => (
<div className="row">
<div className="name">{character.name}</div>
<div className="description">{character.description}</div>
</div>
);
虽然这解决了问题,但我们的设计师认为页面看起来很笨拙,最好折叠这些信息,直到用户需要为止。 我们可以采用三种不同的方法来解决此问题。
setState
方法
在React中实现此目的的最简单方法是使用setState()
将数据存储在组件本身内:
//GoTCharacter.js
export class StatefulCharacterRow extends Component {
constructor() {
super();
this.state = {
show_description: false
}
}
render() {
const {character} = this.props;
return (<div className="row">
<div className="name">{character.name}</div>
<a href="#" onClick={() => this.setState({
show_description: !this.state.show_description})} >
{this.state.show_description ? 'collapse' : 'expand'}
</a>
{this.state.show_description &&
<div className="description">{character.description}</div>}
</div>);
}
};
Redux方法
只要我们要处理的状态仅在组件本地即可,使用setState()
很好。 例如,如果我们要放置一个“全部扩展”功能,那么仅使用React将很难处理此功能。
让我们看看如何将其移至Redux:
// FlickDuck.js
// …
export const toggleCharacterDescription = createAction(
FlixActions.TOGGLE_CHARACTER_DESCRIPTION, (character) => ({character})
);
export default (current_state, action) => {
const state = current_state || default_state;
switch (action.type) {
case FlixActions.TOGGLE_CHARACTER_DESCRIPTION:
return {...state, characters: state.characters.map(char => {
if (char.id === action.payload.character.id) {
return {...char,show_description: !char.show_description};
}
return char;
})}
default:
return state
}
}
// GoTCharactersContainer.js
import { connect } from 'react-redux';
import GoTCharacters from './GoTCharacters';
import {toggleCharacterDescription} from './FlickDuck';
const mapStateToProps = (state) => ({
...state.flick
});
const mapDispatchToProps = (dispatch) => ({
toggleCharacterDescription : (data) => dispatch(toggleCharacterDescription(data))
});
export default connect(mapStateToProps, mapDispatchToProps)(GoTCharacters);
// GoTCharacters.js
const GoTCharacters = ({characters,toggleCharacterDescription}) => {
return (
<div className="characters-list">
{characters.map(char => (
<CharacterRow
character={char}
toggleCharacterDescription={toggleCharacterDescription}
key={char.id}/>
))}
</div>
);
};
export const CharacterRow = ({character, toggleCharacterDescription}) => (
<div className="row">
<div className="name">{character.name}</div>
<a href="#" onClick={toggleCharacterDescription.bind(null, character)} >
{character.show_description ? 'collapse' : 'expand'}
</a>
{character.show_description &&
<div className="description">{character.description}</div>}
</div>
);
我们将描述字段的状态存储在角色对象内部。 现在我们的状态将如下所示:
state = {
characters: [{
id: 1,
name: "Eddard Ned Stark",
house: "stark",
description: "Lord of Winterfell - Warden of the North - Hand of the King - Married to Catelyn (Tully) Stark",
imageSuffix: "eddard-stark",
wikiSuffix: "Eddard_Stark",
show_description: true
},
{
id: 2,
name: "Benjen Stark",
house: "stark",
description: "Brother of Eddard Stark - First ranger of the Night's Watch",
imageSuffix: "benjen-stark",
wikiSuffix: "Benjen_Stark",
show_description: false
}]
}
这是许多开发人员在开始使用Redux时遵循的一般模式。 这种方法没有什么问题,它适用于较小的应用程序。
到目前为止,我们一直在处理GoT第一章中的字符,并且宇宙将变得更大。 如果启用,我们的应用程序将变慢。 想象一下循环遍历1000个字符以更新一行。
让我们看看如何针对更大的数据集进行缩放:
// FlickDuck.js
// …
case FlixActions.TOGGLE_CHARACTER_DESCRIPTION:
const {character} = action.payload;
return {
...state,
character_show_description: {
...state.character_show_description,
[character.id]: !state.character_show_description[character.id]
}
}
// …
在GoTCharacters.js
:
export const CharacterRow = ({character, character_show_description, toggleCharacterDescription}) => (
<div className="row">
<div className="name">{character.name}</div>
<a href="#" onClick={toggleCharacterDescription.bind(null, character)} >
{character_show_description[character.id] ? 'collapse' : 'expand'}
</a>
{character_show_description[character.id] &&
<div className="description">{character.description}</div>}
</div>
);
当在用户点击展开链接,我们更新了character_show_description
当前角色ID。 状态现在看起来像这样:
state = {
characters: [...],
character_show_description: {
1: true,
2: false
}
}
现在,我们可以更新UI状态,而无需遍历所有字符。
在Redux中管理表单状态
管理表单状态是一项棘手的工作。 在典型的应用程序中,我们将在提交过程中对表单数据进行一次序列化,如果有效,则将其提交。 否则,我们将显示错误消息。 随便吧,对吧?
但是,在现实世界中,我们将进行一些涉及表单的复杂交互。 如果表单上存在验证错误,我们可能必须在页面顶部显示错误。 根据UX,我们甚至可能需要禁用页面其他部分的某些元素。 通常可以通过传递父母的父母的父母的随机回调,甚至通过每次验证操作DOM来实现。
让我们看看如何使用Redux来实现它:
// FlickDuck.js
// ============
const FlixActions = km({
FETCH_CHARACTERS: null,
TOGGLE_CHARACTER_DESCRIPTION: null,
TOGGLE_CHARACTER_EDIT: null,
SYNC_CHARACTER_EDIT_DATA: null,
SAVE_CHARACTER_EDIT: null
});
const default_state = {
characters: characters,
character_show_description: {},
show_character_edit: {},
character_edit_form_data: {}
};
export const toggleEdit = createAction(
FlixActions.TOGGLE_CHARACTER_EDIT, (character) => ({character})
);
export const syncCharacterEditData = createAction(
FlixActions.SYNC_CHARACTER_EDIT_DATA, (character, form_data) => ({character, form_data})
);
export const editCharacterDetails = createAction(
FlixActions.SAVE_CHARACTER_EDIT, (character) => ({character})
);
export default (current_state, action) => {
// …
switch (action.type) {
// …
case FlixActions.TOGGLE_CHARACTER_EDIT:
character = action.payload.character;
const show_character_edit = !state.show_character_edit[character.id];
return {
...state,
show_character_edit: {
...state.show_character_edit,
[character.id]: show_character_edit
}, character_edit_form_data : {
...state.character_edit_form_data,
[character.id]: show_character_edit ? {...character} : {}
}
}
case FlixActions.SYNC_CHARACTER_EDIT_DATA:
character = action.payload.character;
const {form_data} = action.payload;
return {
...state,
character_edit_form_data: {
...state.character_edit_form_data,
[character.id]: {...form_data}
}
}
case FlixActions.SAVE_CHARACTER_EDIT:
character = action.payload.character;
const edit_form_data = state.character_edit_form_data[character.id];
const characters = state.characters.map(char => {
if (char.id === character.id) return {...char, name:edit_form_data.name, description: edit_form_data.description}
return char;
});
return {
...state,
characters,
show_character_edit: {
...state.show_character_edit,
[character.id]: false
}
}
// …
}
}
// GotCharacters.js
export const CharacterRow = ({character, character_show_description, character_edit_form_data, show_character_edit, toggleCharacterDescription, toggleEdit, syncCharacterEditData, editCharacterDetails}) => {
const toggleEditPartial = toggleEdit.bind(null, character);
return (<div className="row">
<div className="name">{character.name}</div>
<a href="#" onClick={toggleCharacterDescription.bind(null, character)} >
{character_show_description[character.id] ? 'collapse' : 'expand'}
</a>
{!character_show_description[character.id] && <a href="#" onClick={toggleEditPartial} >
edit
</a>}
{character_show_description[character.id] &&
<div className="description">{character.description}</div>}
{show_character_edit[character.id] &&
<EditCharacterDetails character={character}
cancelEdit={toggleEditPartial}
syncCharacterEditData={syncCharacterEditData}
editCharacterDetails={editCharacterDetails}
edit_data={character_edit_form_data[character.id]}/>
}
</div>);
}
export const EditCharacterDetails = ({character, edit_data, syncCharacterEditData, editCharacterDetails, cancelEdit}) => {
const syncFormData = (key, e) => {
const {value} = e.currentTarget;
syncCharacterEditData(character, {
...edit_data,
[key]: value
});
};
const saveForm = (e) => {
e.preventDefault();
editCharacterDetails(character);
};
return (
<form onSubmit={saveForm}>
<label>Name: </label>
<input name='name' value={edit_data.name} onChange={syncFormData.bind(null, 'name')}/>
<label>Description:</label>
<textarea name='description' value={edit_data.description} onChange={syncFormData.bind(null, 'description')}/>
<button type="reset" onClick={cancelEdit}> Cancel </button>
<button type="submit"> Submit </button>
</form>
);
};
让我们将其扩展为处理验证:
// FlickDuck.js
// ============
export const editCharacterDetails = createAction(
FlixActions.VALIDATE_AND_SAVE_CHARACTER_EDIT, (dispatch, character, edit_form_data) => {
const errors = validateCharacterForm(edit_form_data);
if (Object.keys(errors).length) {
return dispatch(showErrorMessage(character, errors));
}
return dispatch(saveCharacterEdit(character));
}
);
export const showErrorMessage = createAction(
FlixActions.VALIDATE_CHARACTER_EDIT, (character, errors) => ({character, errors, hasError: true})
);
export const saveCharacterEdit = createAction(
FlixActions.SAVE_CHARACTER_EDIT, (character) => ({character})
);
switch (action.type) {
// …
case FlixActions.VALIDATE_CHARACTER_EDIT:
character = action.payload.character;
const {errors, hasError} = action.payload;
return {
...state,
character_edit_form_errors: {
...state.character_edit_form_errors,
[character.id]: {errors, hasError}
}
}
// …
}
这与我们在上一节中看到的示例非常相似吗? 表格有什么特别之处?
在开始之前,了解Redux内部原理是很重要的。 当状态更改时,您不会更新树中的单个点。 而是将整个状态树替换为新的状态树。 该树将传递给您的React组件,React协调所有组件以查看DOM是否需要更新。
表单状态很特殊,因为状态树变化非常快。 根据用户的键入速度,这可能是一个问题。 由于状态更改会触发所有节点的对帐,因此在用户键入时可能会有小的延迟。 当处理包含数百个组件的大页面时,它会变得非常明显。
让我们看看如何在不做大改变的情况下重塑它:
export class StatefulCharacterRow extends Component {
constructor() {
super();
this.toggleEditForm = this.toggleEditForm.bind(this);
this.syncCharacterEditData = this.syncCharacterEditData.bind(this);
this.state = {
show_description: false,
show_edit_form: false,
edit_data: {}
}
}
toggleEditForm() {
const {name, description} = this.props.character;
const show_edit_form = !this.state.show_edit_form;
const edit_data = show_edit_form ? {name, description} : {};
this.setState({show_edit_form, edit_data});
}
syncCharacterEditData(character, form_data) {
this.setState({
edit_data: {...this.state.edit_data, ...form_data}
});
}
render() {
const {character} = this.props;
return (<div className="row">
<div className="name">{character.name}</div>
<a href="#" onClick={() => this.setState({
show_description: !this.state.show_description})} >
{this.state.show_description ? 'collapse' : 'expand'}
</a>
{!this.state.show_edit_form && <a href="#" onClick={this.toggleEditForm} >
edit
</a>}
{this.state.show_description &&
<div className="description">{character.description}</div>}
{this.state.show_edit_form &&
<EditCharacterDetails character={character}
cancelEdit={this.toggleEditForm}
syncCharacterEditData={this.syncCharacterEditData}
editCharacterDetails={this.props.editCharacterDetails}
edit_data={this.state.edit_data}/> }
</div>);
}
};
处理此问题的最简单方法是在表单周围创建一个包装器组件(将其视为容器)并将状态存储在那里。 因此,当用户输入更改时,仅更新此节点而不会摇动整个树。
注意,我们仅将表单状态移到了React内,但错误状态仍在外面。 如果我们想在表单范围之外处理这些错误,这将有助于减少不必要的混乱。
包起来
在决定使用Redux时在何处存储状态之前,了解以下情况将很有帮助:
1.是此UI状态还是应用程序状态?
角色名称是应用程序状态,而跟踪操作是否正在进行是UI状态。 虽然很有吸引力,但长远来看,将它们分开是值得的。
state = {
characters: [{
id: 1,
name: Jon Snow,
…
}],
ui_state: {
1: {
is_edit_in_progress: true,
show_description: false
}
}
}
2.如何确定组件状态和Redux中的内容
通常,应用程序数据可以在页面上呈现多次。 例如,我们可以呈现所有字符的列表,并显示按字符所属房屋分组的字符计数。 在Redux中管理它们很有意义。
如果存在全局依赖性,则将UI状态存储在Redux中。 否则,最好使用React的本地组件状态来处理它。
Redux帮助我更好地组织了思想。 使用jQuery / Backbone,我的重点是如何操纵DOM来达到预期的效果。 借助Redux,它可以使您的应用程序状态正确无误。 一旦确定了这一点,前端代码库的复杂性就会大大降低。
From: https://www.sitepoint.com/redux-not-art-structuring-state-react-apps/