React 状态管理与进阶

在前面的章节中,你已经学习了 React 中状态管理的基础知识,本章将会深入这个话题。你将学习到状态管理的最佳实践,如何去应用它们以及为什么可以考虑使用第三方的状态管理库。

状态提取

在你的应用中,只有 App 是具有状态的 ES6 组件。在该组件的方法中,包含了许多应用的状态和业务的处理逻辑。可能你已经注意到了,我们给 Table 组件传入了大量属性。而这些参数中的绝大部分只有在 Table 组件中才被用到。所以有人可能会得出“App 组件不需要知道这些参数”的结论。

整个排序功能只有在 Table 组件中用到了,你可以将其移动到 Table 组件中,因为 App 组件根本不需要了解这些信息。将子状态(substate)从一个组件移动到其他组件中的重构过程被称为状态提取。在这里,你想要将 App 组件中用不到的状态移动到 Table 组件中。这里的状态是从父组件到子组件向下移动。

为了能够在 Table 组件中管理状态和添加方法,需要将其改写成 ES6 类的形式。从函数式无状态组件(functional stateless component)到 ES6 类形式组件的重构非常简单明了。

函数式无状态组件形式的 Table 组件:

{title=”src/App.js”,lang=javascript}

  1. const Table = ({
  2. list,
  3. sortKey,
  4. isSortReverse,
  5. onSort,
  6. onDismiss
  7. }) => {
  8. const sortedList = SORTS[sortKey](list);
  9. const reverseSortedList = isSortReverse
  10. ? sortedList.reverse()
  11. : sortedList;
  12. return(
  13. ...
  14. );
  15. }

ES6 类形式的 Table 组件:

{title=”src/App.js”,lang=javascript}

  1. # leanpub-start-insert
  2. class Table extends Component {
  3. render() {
  4. const {
  5. list,
  6. sortKey,
  7. isSortReverse,
  8. onSort,
  9. onDismiss
  10. } = this.props;
  11. const sortedList = SORTS[sortKey](list);
  12. const reverseSortedList = isSortReverse
  13. ? sortedList.reverse()
  14. : sortedList;
  15. return(
  16. ...
  17. );
  18. }
  19. }
  20. # leanpub-end-insert

由于想要在 Table 组件中管理状态,你需要添加构造函数和初始状态。

{title=”src/App.js”,lang=javascript}

  1. class Table extends Component {
  2. # leanpub-start-insert
  3. constructor(props) {
  4. super(props);
  5. this.state = {};
  6. }
  7. # leanpub-end-insert
  8. render() {
  9. ...
  10. }
  11. }

现在你可以将状态和有关排序的方法从 App 组件向下移动到 Table 组件中。

{title=”src/App.js”,lang=javascript}

  1. class Table extends Component {
  2. constructor(props) {
  3. super(props);
  4. # leanpub-start-insert
  5. this.state = {
  6. sortKey: 'NONE',
  7. isSortReverse: false,
  8. };
  9. this.onSort = this.onSort.bind(this);
  10. # leanpub-end-insert
  11. }
  12. # leanpub-start-insert
  13. onSort(sortKey) {
  14. const isSortReverse = this.state.sortKey === sortKey && !this.state.isSortReverse;
  15. this.setState({ sortKey, isSortReverse });
  16. }
  17. # leanpub-end-insert
  18. render() {
  19. ...
  20. }
  21. }

别忘了将挪走的状态和 onSort() 方法从 App 组件中移除。

{title=”src/App.js”,lang=javascript}

  1. class App extends Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {
  5. results: null,
  6. searchKey: '',
  7. searchTerm: DEFAULT_QUERY,
  8. error: null,
  9. isLoading: false,
  10. };
  11. this.setSearchTopStories = this.setSearchTopStories.bind(this);
  12. this.fetchSearchTopStories = this.fetchSearchTopStories.bind(this);
  13. this.onDismiss = this.onDismiss.bind(this);
  14. this.onSearchSubmit = this.onSearchSubmit.bind(this);
  15. this.onSearchChange = this.onSearchChange.bind(this);
  16. this.needsToSearchTopStories = this.needsToSearchTopStories.bind(this);
  17. }
  18. ...
  19. }

除此之外,你还可以让 Table 组件更加轻量。你还可以去掉从 App 组件传入的属性,因为现在这些属性可以由 Table 组件的内部状态控制。

{title=”src/App.js”,lang=javascript}

  1. class App extends Component {
  2. ...
  3. render() {
  4. # leanpub-start-insert
  5. const {
  6. searchTerm,
  7. results,
  8. searchKey,
  9. error,
  10. isLoading
  11. } = this.state;
  12. # leanpub-end-insert
  13. ...
  14. return (
  15. <div className="page">
  16. ...
  17. # leanpub-start-insert
  18. <Table
  19. list={list}
  20. onDismiss={this.onDismiss}
  21. />
  22. # leanpub-end-insert
  23. ...
  24. </div>
  25. );
  26. }
  27. }

现在你就可以使用 Table 组件内的 onSort() 方法和状态了。

{title=”src/App.js”,lang=javascript}

  1. class Table extends Component {
  2. ...
  3. render() {
  4. # leanpub-start-insert
  5. const {
  6. list,
  7. onDismiss
  8. } = this.props;
  9. const {
  10. sortKey,
  11. isSortReverse,
  12. } = this.state;
  13. # leanpub-end-insert
  14. const sortedList = SORTS[sortKey](list);
  15. const reverseSortedList = isSortReverse
  16. ? sortedList.reverse()
  17. : sortedList;
  18. return(
  19. <div className="table">
  20. <div className="table-header">
  21. <span style={{ width: '40%' }}>
  22. <Sort
  23. sortKey={'TITLE'}
  24. # leanpub-start-insert
  25. onSort={this.onSort}
  26. # leanpub-end-insert
  27. activeSortKey={sortKey}
  28. >
  29. Title
  30. </Sort>
  31. </span>
  32. <span style={{ width: '30%' }}>
  33. <Sort
  34. sortKey={'AUTHOR'}
  35. # leanpub-start-insert
  36. onSort={this.onSort}
  37. # leanpub-end-insert
  38. activeSortKey={sortKey}
  39. >
  40. Author
  41. </Sort>
  42. </span>
  43. <span style={{ width: '10%' }}>
  44. <Sort
  45. sortKey={'COMMENTS'}
  46. # leanpub-start-insert
  47. onSort={this.onSort}
  48. # leanpub-end-insert
  49. activeSortKey={sortKey}
  50. >
  51. Comments
  52. </Sort>
  53. </span>
  54. <span style={{ width: '10%' }}>
  55. <Sort
  56. sortKey={'POINTS'}
  57. # leanpub-start-insert
  58. onSort={this.onSort}
  59. # leanpub-end-insert
  60. activeSortKey={sortKey}
  61. >
  62. Points
  63. </Sort>
  64. </span>
  65. <span style={{ width: '10%' }}>
  66. Archive
  67. </span>
  68. </div>
  69. { reverseSortedList.map((item) =>
  70. ...
  71. )}
  72. </div>
  73. );
  74. }
  75. }

应用应该还是可以像之前一样正常运行,但是你已经做了非常重要的重构工作。相关的逻辑代码和状态信息从 App 组件移动到了 Table 组件中,这使得 App 组件更加轻量。此外因为 Table 的排序逻辑放在了组件内部,所以它的接口也更加轻量了。

状态提取的过程也可以反过来:从子组件到父组件,这种情形被称为状态提升。想象一下,你在子组件中处理了内部的状态信息。现在为了满足新的需求,在其父组件中也显示该组件的状态信息,你需要将状态提升到父组件中。但情况还不止这些,假如你需要在子组件的兄弟组件上显示该组件的状态,你还是需要将状态提升到父组件中。在父组件中处理内部状态,同时将状态信息暴露给相关的子组件。

练习:

再探:setState()

至此,你已经使用过 React 的 setState() 方法来管理组件的内部状态。你可以给该函数传入一个对象来改变部分的内部状态。

{title=”Code Playground”,lang=”javascript”}

  1. this.setState({ foo: bar });

但是 setState() 方法不仅可以接收对象。在它的第二种形式中,你还可以传入一个函数来更新状态信息。

{title=”Code Playground”,lang=”javascript”}

  1. this.setState((prevState, props) => {
  2. ...
  3. });

为什么你会需要第二种形式呢?使用函数作为参数而不是对象,有一个非常重要的应用场景,就是当更新状态需要取决于之前的状态或者属性的时候。如果不使用函数参数的形式,组件的内部状态管理可能会引起 bug。

当更新状态需要取决于之前的状态或者属性时,为什么使用对象而不是函数会引起 bug 呢?这是因为 React 的 setState() 方法是异步的。React 依次执行 setState() 方法,最终会全部执行完毕。如果你的 setState() 方法依赖于之前的状态或者属性的话,有可能在按批次执行的期间,状态或者属性的值就已经被改变了。

{title=”Code Playground”,lang=”javascript”}

  1. const { fooCount } = this.state;
  2. const { barCount } = this.props;
  3. this.setState({ count: fooCount + barCount });

想象一下像 fooCountbarCount 这样的状态或属性,在你调用 setState() 方法的时候在其他地方被异步地改变了。在不断膨胀的应用中,你会有多个 setState() 调用。因为 setState() 是异步执行的,你可能像上面的例子一样,依赖了一个已经过期的值。

使用函数参数形式的话,传入 setState() 方法的参数是一个回调,该回调会在被执行时传入状态和属性。尽管 setState() 方法是异步的,但是通过回调函数,它使用的是执行那一刻的状态和属性。

{title=”Code Playground”,lang=”javascript”}

  1. this.setState((prevState, props) => {
  2. const { fooCount } = prevState;
  3. const { barCount } = props;
  4. return { count: fooCount + barCount };
  5. });

现在让我们回到代码中来修复这个问题。我们会一起修复一个 setState() 依赖于状态和属性的地方,之后你就可以按照同样的方式修复代码中的其他地方。

setSearchTopStories() 方法依赖于之前的状态,因此它是个使用函数而不是对象作为 setState() 参数的绝佳例子。目前的代码片段如下。

{title=”src/App.js”,lang=javascript}

  1. setSearchTopStories(result) {
  2. const { hits, page } = result;
  3. const { searchKey, results } = this.state;
  4. const oldHits = results && results[searchKey]
  5. ? results[searchKey].hits
  6. : [];
  7. const updatedHits = [
  8. ...oldHits,
  9. ...hits
  10. ];
  11. this.setState({
  12. results: {
  13. ...results,
  14. [searchKey]: { hits: updatedHits, page }
  15. },
  16. isLoading: false
  17. });
  18. }

你从 state 变量中提取了一些值,但是更新状态时异步地依赖于之前的状态。现在你可以使用函数参数的形式来防止脏状态信息造成的 bug。

{title=”src/App.js”,lang=javascript}

  1. setSearchTopStories(result) {
  2. const { hits, page } = result;
  3. # leanpub-start-insert
  4. this.setState(prevState => {
  5. ...
  6. });
  7. # leanpub-end-insert
  8. }

你可以将已经实现的逻辑移动到函数内部,只需将在 this.state 上的操作改为 prevState

{title=”src/App.js”,lang=javascript}

  1. setSearchTopStories(result) {
  2. const { hits, page } = result;
  3. this.setState(prevState => {
  4. # leanpub-start-insert
  5. const { searchKey, results } = prevState;
  6. const oldHits = results && results[searchKey]
  7. ? results[searchKey].hits
  8. : [];
  9. const updatedHits = [
  10. ...oldHits,
  11. ...hits
  12. ];
  13. return {
  14. results: {
  15. ...results,
  16. [searchKey]: { hits: updatedHits, page }
  17. },
  18. isLoading: false
  19. };
  20. # leanpub-end-insert
  21. });
  22. }

如此可以修复脏状态所导致的问题。还有一个可以改进的地方,由于它是一个函数,你可以将该函数提取出来从而改善代码的可读性。这是使用函数参数形式相对于对象形式的另一个好处,该函数可以独立于组件。但是你需要使用一个高阶函数并将 result 传给它。毕竟,你是想根据 API 的获取结果来更新状态。

{title=”src/App.js”,lang=javascript}

  1. setSearchTopStories(result) {
  2. const { hits, page } = result;
  3. this.setState(updateSearchTopStoriesState(hits, page));
  4. }

updateSearchTopStoriesState() 是一个高阶函数,因为它返回一个函数。你可以在 App 组件之外定义这个高阶函数。请注意现在函数的签名有了一些变化。

{title=”src/App.js”,lang=javascript}

  1. # leanpub-start-insert
  2. const updateSearchTopStoriesState = (hits, page) => (prevState) => {
  3. const { searchKey, results } = prevState;
  4. const oldHits = results && results[searchKey]
  5. ? results[searchKey].hits
  6. : [];
  7. const updatedHits = [
  8. ...oldHits,
  9. ...hits
  10. ];
  11. return {
  12. results: {
  13. ...results,
  14. [searchKey]: { hits: updatedHits, page }
  15. },
  16. isLoading: false
  17. };
  18. };
  19. # leanpub-end-insert
  20. class App extends Component {
  21. ...
  22. }

搞定!setState() 中函数参数形式相比于对象参数来说,在预防潜在 bug 的同时,还可以提高代码的可读性和可维护性。此外,它可以在 App 组件之外进行测试。你可以将其导出并写个测试来当作练习。

练习:

  • 了解更多关于在 React 中正确使用 state 的内容
  • 将所有使用 setState() 方法的地方重构为函数参数形式
    • 只重构那些需要的地方,即依赖于之前的属性或者状态
  • 重新跑一遍测试,确保一切正常工作

驾驭 State

前面的章节已经说明,状态管理在大型的应用中是一个至关重要的话题。总体来说,不光是 React ,很多单页面应用(SPA)框架都面临这个问题。近些年来应用变得越来越复杂。当今的 web 应用面临的一个重大挑战就是如何驾驭和控制状态。

与其他的解决方案相比,React 已经向前迈进了一大步。单向数据流和简单的组件状态管理 API 非常必要 。这些概念使得推断状态和其改变更加容易,在组件级别以及一定程度上的应用级别的状态推断也更加容易。

在不断膨胀的应用中,推断状态的变化随之变得困难。setState() 方法使用对象形式而不是函数形式的话,如果在脏状态上进行操作,则可能会引入 bug。为了能够共享状态或者在兄弟组件之间隐藏不必要的状态,你需要将状态进行提升或者降低。有些状况下,组件需要将其状态提升,因为它的兄弟组件依赖于这些状态。也有可能你需要和相隔甚远的组件共享状态,所以你可能需要在其整个组件树中共享该状态。这样做的结果会使得在状态管理中涉及的组件范围很广。但是毕竟组件的主要职责只是描绘 UI,不是吗?

由于这些原因,存在一些独立的解决方案来解决状态管理问题。这些方案不仅仅可以在 React 中使用,但是却使得 React 的生态更加繁荣。你可以使用不同的解决方案来解决你的问题。为了解决规模化的状态管理问题,你可能已经听说过 Redux 或者 MobX。你可以在 React 应用中使用这两者其一。它们还有一些扩展,如 react-reduxmobx-react 来将其连接到 React 的视图层。

Redux 和 MobX 超出了本书的讨论范围。当读读完本书的时候,你将获得关于如何继续学习 React 及其生态系统的指导。其中一个学习路线是 Redux。在你深入外部状态管理主题之前,我推荐你阅读这篇文章。它旨在给你一个关于如何学习外部状态管理的更好理解。

练习:

{pagebreak}

你已经学习了 React 的高级状态管理!让我们回顾一下前面几章的内容。

  • React
    • 将状态提升或者下降到合适的组件中
    • setState 方法可以使用函数参数形式来阻止脏状态的 bug
    • 存在外部的解决方案帮助你掌握驾驭 state

你可以从 官方代码仓库 中找到源代码。