• React 状态管理与进阶
    • 状态提取
      • 练习:
    • 再探:setState()
      • 练习:
    • 驾驭 State
      • 练习:

    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 的排序逻辑放在了组件内部,所以它的接口也更加轻量了。

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

    练习:

    • 了解更多关于 React 的状态提升 的内容
    • 在使用 Redux 之前学习 React这篇文章中了解更多关于状态提升的信息

    再探: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-redux 和 mobx-react 来将其连接到 React 的视图层。

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

    练习:

    • 阅读更多关于 外部状态管理以及如何学习 的内容
    • 看看我的第二本电子书关于 React 中的状态管理

    {pagebreak}

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

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

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