是否进行Redux:在React Apps中构建状态的艺术

我在大多数Redux开发人员中发现的一个普遍趋势是对setState()的仇恨。 我们中的许多人(是的,我之前多次陷入这个陷阱)在setState()面前退缩,并尝试将所有数据保留在我们的Redux存储中。 但是,随着应用程序复杂性的增加,这带来了一些挑战。

在本文中,我将引导您完成各种用于建模状态的策略,并深入探讨何时可以使用它们。

入门

Redux的原则是成为应用程序状态的唯一事实来源。 新的《权力的游戏》(Game of Thrones)季节即将播出,我敢肯定,每个人都很高兴知道这种情况将如何发展。 让我们构建一个有趣的《权力的游戏》粉丝列表页面,以详细了解这些概念。

注意:我将使用yarn运行该应用程序。 如果没有设置纱线,则将纱线替换为npm

之前,我们在跳水,从下载的基本骨架回购和运行:

yarn install
yarn run start

您应该看到一个基本列表页面,其中列出了一些您喜欢的GoT字符。

注意:我们将使用ducks模式编写应用程序。 它减少了不必要的模块导入,并减少了许多样板。

Redux简介

本文的范围是为了帮助您构建Redux应用程序。 它假定了图书馆的基本知识。 我将简要介绍Redux概念,以帮助您更好地遵循本文的其余部分。 如果您熟悉这些方法的工作原理,请随时跳过本节。

所有Redux应用程序都使用四个重要的结构:动作,reduce,商店和容器。

动作

动作是为了更新状态。 它可能是由网络呼叫或用户单击按钮触发的。 动作分为两个部分:

  1. 动作类型 。 代表动作的唯一标识符。
  2. 有效载荷 。 与操作关联的任何元数据。 例如,如果我们发出网络请求以获取电影列表,则服务器的响应就是有效负载。

在此示例中,我们将使用称为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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值