• 高级React组件
    • 引用DOM元素
      • 练习
    • 加载 ……
      • 练习:
    • 高阶组件
      • 练习:
    • 高级排序
      • 练习:

    高级React组件

    本章将重点介绍高级 React 组件的实现。我们将了解什么是高阶组件以及如何实现它们。此外,我们还将深入探讨 React 中更高级的主题,并用它实现复杂的交互功能。

    引用DOM元素

    有时我们需要在 React 中与 DOM 节点进行交互。ref属性可以让我们访问元素中的一个节点。通常,访问 DOM 节点是 React 中的一种反模式,因为我们应该遵循它的声明式编程和单向数据流。当我们引入第一个搜索输入组件时,就已经了解这些了。但是在某些情况下,我们仍然需要访问 DOM 节点。官方文档提到了三种情况:

    • 使用 DOM API(focus事件,媒体播放等)
    • 调用命令式 DOM 节点动画
    • 与需要 DOM 节点的第三方库集成(例如 D3.JavaScript)

    让我们通过 Search 组件这个例子看一下。当应用程序第一次渲染时,input 字段应该被聚焦。这是需要访问 DOM API 的一种用例。本章将展示渲染时聚焦 input 字段是如何工作的,但由于这个功能对于应用程序并不是很有用,所以我们将在本章之后省略这些更改。尽管如此,你仍然可以为自己的应用程序保留它。

    通常,无状态组件和 ES6 类组件中都可以使用 ref 属性。在聚焦 input 字段的用例中,我们就需要一个生命周期方法。这就是为什么接下来会先在 ES6 类组件中展示如何使用 ref 属性。

    第一步是将无状态组件重构为 ES6 类组件。

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

    1. # leanpub-start-insert
    2. class Search extends Component {
    3. render() {
    4. const {
    5. value,
    6. onChange,
    7. onSubmit,
    8. children
    9. } = this.props;
    10. return (
    11. # leanpub-end-insert
    12. <form onSubmit={onSubmit}>
    13. <input
    14. type="text"
    15. value={value}
    16. onChange={onChange}
    17. />
    18. <button type="submit">
    19. {children}
    20. </button>
    21. </form>
    22. # leanpub-start-insert
    23. );
    24. }
    25. }
    26. # leanpub-end-insert

    ES6 类组件的this对象可以帮助我们通过ref属性引用 DOM 节点。

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

    1. class Search extends Component {
    2. render() {
    3. const {
    4. value,
    5. onChange,
    6. onSubmit,
    7. children
    8. } = this.props;
    9. return (
    10. <form onSubmit={onSubmit}>
    11. <input
    12. type="text"
    13. value={value}
    14. onChange={onChange}
    15. # leanpub-start-insert
    16. ref={(node) => { this.input = node; }}
    17. # leanpub-end-insert
    18. />
    19. <button type="submit">
    20. {children}
    21. </button>
    22. </form>
    23. );
    24. }
    25. }

    现在,你可以通过使用 this 对象、适当的生命周期方法和 DOM API 在组件挂载的时候来聚焦 input 字段。

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

    1. class Search extends Component {
    2. # leanpub-start-insert
    3. componentDidMount() {
    4. if(this.input) {
    5. this.input.focus();
    6. }
    7. }
    8. # leanpub-end-insert
    9. render() {
    10. const {
    11. value,
    12. onChange,
    13. onSubmit,
    14. children
    15. } = this.props;
    16. return (
    17. <form onSubmit={onSubmit}>
    18. <input
    19. type="text"
    20. value={value}
    21. onChange={onChange}
    22. ref={(node) => { this.input = node; }}
    23. />
    24. <button type="submit">
    25. {children}
    26. </button>
    27. </form>
    28. );
    29. }
    30. }

    当应用程序渲染时,input 字段应该被聚焦。这就是ref属性的基本用法。

    但是我们怎样在没有this对象的无状态组件中访问ref属性呢?接下来我们在无状态组件中演示。

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

    1. const Search = ({
    2. value,
    3. onChange,
    4. onSubmit,
    5. children
    6. }) => {
    7. # leanpub-start-insert
    8. let input;
    9. # leanpub-end-insert
    10. return (
    11. <form onSubmit={onSubmit}>
    12. <input
    13. type="text"
    14. value={value}
    15. onChange={onChange}
    16. # leanpub-start-insert
    17. ref={(node) => input = node}
    18. # leanpub-end-insert
    19. />
    20. <button type="submit">
    21. {children}
    22. </button>
    23. </form>
    24. );
    25. }

    现在我们能够访问 input DOM 元素。由于在无状态组件中,没有生命周期方法去触发聚焦事件,这个功能对于聚焦 input 字段这个用例而言没什么用。但是在将来,你可能会遇到其他一些合适的需要在无状态组件中使用ref属性的情况。

    练习

    • 阅读 React中的ref属性概述
    • 阅读 在React中使用ref属性

    加载 ……

    现在让我们回到应用程序。当向 Hacker News API 发起搜索请求时,我们想要显示一个加载指示符。

    请求是异步的,此时应该向用户展示某些事情即将发生的某种反馈。让我们在 src/App.js 中定义一个可重用的 Loading 组件。

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

    1. const Loading = () =>
    2. <div>Loading ...</div>

    现在我们需要存储加载状态 (isLoading)。根据加载状态 (isLoading),决定是否显示 Loading 组件。

    {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. # leanpub-start-insert
    10. isLoading: false,
    11. # leanpub-end-insert
    12. };
    13. ...
    14. }
    15. ...
    16. }

    isLoading 的初始值是 false。在 App 组件挂载完成之前,无需加载任何东西。

    当发起请求时,将加载状态 (isLoading) 设置为 true。最终,请求会成功,那时可以将加载状态 (isLoading) 设置为 false。

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

    1. class App extends Component {
    2. ...
    3. setSearchTopStories(result) {
    4. ...
    5. this.setState({
    6. results: {
    7. ...results,
    8. [searchKey]: { hits: updatedHits, page }
    9. },
    10. # leanpub-start-insert
    11. isLoading: false
    12. # leanpub-end-insert
    13. });
    14. }
    15. fetchSearchTopStories(searchTerm, page = 0) {
    16. # leanpub-start-insert
    17. this.setState({ isLoading: true });
    18. # leanpub-end-insert
    19. fetch(`${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${searchTerm}&${PARAM_PAGE}${page}&${PARAM_HPP}${DEFAULT_HPP}`)
    20. .then(response => response.json())
    21. .then(result => this.setSearchTopStories(result))
    22. .catch(e => this.setState({ error: e }));
    23. }
    24. ...
    25. }

    最后一步,我们将在应用程序中使用 Loading 组件。基于加载状态 (isLoading) 的条件来决定渲染 Loading 组件或 Button 组件。后者为一个用于获取更多数据的按钮。

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

    1. class App extends Component {
    2. ...
    3. render() {
    4. const {
    5. searchTerm,
    6. results,
    7. searchKey,
    8. error,
    9. # leanpub-start-insert
    10. isLoading
    11. # leanpub-end-insert
    12. } = this.state;
    13. ...
    14. return (
    15. <div className="page">
    16. ...
    17. <div className="interactions">
    18. # leanpub-start-insert
    19. { isLoading
    20. ? <Loading />
    21. : <Button
    22. onClick={() => this.fetchSearchTopStories(searchKey, page + 1)}>
    23. More
    24. </Button>
    25. }
    26. # leanpub-end-insert
    27. </div>
    28. </div>
    29. );
    30. }
    31. }

    由于我们在componentDidMount()中发起请求,Loading 组件会在应用程序启动的时候显示。此时,因为列表是空的,所以不显示 Table 组件。当响应数据从 Hacker News API 返回时,返回的数据会通过 Table 组件显示出来,加载状态 (isLoading) 设置为 false,然后 Loading 组件消失。同时,出现了可以获取更多的数据的“More”按钮。一旦点击按钮,获取更多的数据,该按钮将消失,加载组件会重新出现。

    练习:

    • 使用第三方库,比如Font Awesome,来显示加载图标,而不是“Loading …”文本

    高阶组件

    高阶组件(HOC)是 React 中的一个高级概念。HOC 与高阶函数是等价的。它接受任何输入 - 多数时候是一个组件,也可以是可选参数 - 并返回一个组件作为输出。返回的组件是输入组件的增强版本,并且可以在JSX中使用。

    HOC可用于不同的情况,比如:准备属性,管理状态或更改组件的表示形式。其中一种情况是将 HOC 用于帮助实现条件渲染。想象一下现在有一个 List 组件,由于列表可以为空或无,那么它可以渲染一个列表或者什么也不渲染。当没有列表的时候,HOC 可以屏蔽掉这个不显示任何内容的列表。另一方面,这个简单的 List 组件不再需要关心列表存不存在,它只关心渲染列表。

    我们接下来创建一个简单的 HOC,它将一个组件作为输入并返回一个组件。我们可以把它放在 src / App.js 文件中。

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

    1. function withFoo(Component) {
    2. return function(props) {
    3. return <Component { ...props } />;
    4. }
    5. }

    有一个惯例是用 “with” 前缀来命名 HOC。由于我们现在使用的是 ES6,因此可以使用 ES6 箭头函数更简洁地表达 HOC。

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

    1. const withFoo = (Component) => (props) =>
    2. <Component { ...props } />

    在这个例子中,没有做任何改变,输入组件将和输出组件一样。它渲染与输入组件相同的实例,并将所有的属性(props)传递给输出组件,但是这个 HOC 没意义。我们来增强输出组件功能:当加载状态 (isLoading) 为 true 时,组件显示 Loading 组件,否则显示输入的组件。条件渲染是 HOC 的一种绝佳用例。

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

    1. # leanpub-start-insert
    2. const withLoading = (Component) => (props) =>
    3. props.isLoading
    4. ? <Loading />
    5. : <Component { ...props } />
    6. # leanpub-end-insert

    基于加载属性 (isLoading),我们可以实现条件渲染。该函数将返回 Loading 组件或输入的组件。

    一般来说,将对象展开然后作为一个组件的输入是非常高效的(比如说前面那个例子中的 props 对象)。请参阅下面的代码片段中的区别。

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

    1. // before you would have to destructure the props before passing them
    2. const { foo, bar } = props;
    3. <SomeComponent foo={foo} bar={bar} />
    4. // but you can use the object spread operator to pass all object properties
    5. <SomeComponent { ...props } />

    有一点应该避免。我们把包括isLoading属性在内的所有 props 通过展开对象传递给输入的组件。

    然而,输入的组件可能不关心isLoading属性。我们可以使用 ES6 中的 rest 解构来避免它。

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

    1. # leanpub-start-insert
    2. const withLoading = (Component) => ({ isLoading, ...rest }) =>
    3. isLoading
    4. ? <Loading />
    5. : <Component { ...rest } />
    6. # leanpub-end-insert

    这段代码从 props 对象中取出一个属性,并保留剩下的属性。这也适用于多个属性。你可能已经在 解构赋值中了解过它。

    现在,我们已在 JSX 中使用 HOC。应用程序中的用例可能是显示 “More” 按钮或 Loading 组件。

    Loading 组件已经封装在 HOC 中,缺失了输入组件。在显示 Button 组件或 Loading 组件的用例中,Button 是 HOC 的输入组件。增强的输出组件是一个 ButtonWithLoading 的组件。

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

    1. const Button = ({ onClick, className = '', children }) =>
    2. <button
    3. onClick={onClick}
    4. className={className}
    5. type="button"
    6. >
    7. {children}
    8. </button>
    9. const Loading = () =>
    10. <div>Loading ...</div>
    11. const withLoading = (Component) => ({ isLoading, ...rest }) =>
    12. isLoading
    13. ? <Loading />
    14. : <Component { ...rest } />
    15. # leanpub-start-insert
    16. const ButtonWithLoading = withLoading(Button);
    17. # leanpub-end-insert

    现在所有的东西已经被定义好了。最后一步,就是使用 ButtonWithLoading 组件,它接收加载状态 (isLoading) 作为附加属性。当 HOC 消费加载属性 (isLoading) 时,再将所有其他 props 传递给 Button 组件。

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

    1. class App extends Component {
    2. ...
    3. render() {
    4. ...
    5. return (
    6. <div className="page">
    7. ...
    8. <div className="interactions">
    9. # leanpub-start-insert
    10. <ButtonWithLoading
    11. isLoading={isLoading}
    12. onClick={() => this.fetchSearchTopStories(searchKey, page + 1)}>
    13. More
    14. </ButtonWithLoading>
    15. # leanpub-end-insert
    16. </div>
    17. </div>
    18. );
    19. }
    20. }

    当再次运行测试时,App 组件的快照测试会失败。执行 diff 在命令行可能显示如下:

    {title=”Command Line”,lang=”text”}

    1. - <button
    2. - className=""
    3. - onClick={[Function]}
    4. - type="button"
    5. - >
    6. - More
    7. - </button>
    8. + <div>
    9. + Loading ...
    10. + </div>

    如果你认为是 App 组件有问题,现在可以选择修复该组件,或者选择接受 App 组件的新快照。因为本章介绍了 Loading 组件,我们可以在交互测试的命令行中接受已经更改的快照测试。

    高阶组件是 React 中的高级技术。它可以使组件具有更高的重用性,更好的抽象性,更强的组合性,以及提升对 props,state 和视图的可操作性。如果不能马上理解,别担心。我们需要时间去熟悉它。

    我们推荐阅读高阶组件的简单介绍。这篇文章介绍了另一种学习高阶组件的方法,展示了如何用函数式的方式定义高阶组件并优雅的使用它,以及使用高阶组件解决条件渲染的问题。

    练习:

    • 阅读 高阶组件的简单介绍
    • 使用创建的高阶组件
    • 思考一个适合使用高阶组件的场景
      • 如果想到了使用场景,请实现这个高阶组件

    高级排序

    我们已经实现了客户端和服务器端搜索交互。因为我们已经拥了 Table 组件,所以增强 Table 组件的交互性是有意义的。那接下来,我们为 Table 组件加入根据列标题进行排序的功能如何?

    你自己写一个排序函数,但是一般这种情况,我个人更喜欢使用第三方工具库。lodash就是这些工具库之一,当然你也可以选择适用于你的任何第三方库。让我们安装 lodash 并使用。

    {title=”Command Line”,lang=”text”}

    1. npm install lodash

    现在我们可以在 src/App 文件中导入lodash的sort方法。

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

    1. import React, { Component } from 'react';
    2. import fetch from 'isomorphic-fetch';
    3. # leanpub-start-insert
    4. import { sortBy } from 'lodash';
    5. # leanpub-end-insert
    6. import './App.css';

    Table 组件中有好几列,分别是标题,作者,评论和评分。你可以定义排序函数,而每个函数接受一个列表并返回按照指定属性排序过的列表。此外,我们还需要一个默认的排序函数,该函数不做排序而只是用于返回未排序的列表。这将作为组件的初始状态。

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

    1. ...
    2. # leanpub-start-insert
    3. const SORTS = {
    4. NONE: list => list,
    5. TITLE: list => sortBy(list, 'title'),
    6. AUTHOR: list => sortBy(list, 'author'),
    7. COMMENTS: list => sortBy(list, 'num_comments').reverse(),
    8. POINTS: list => sortBy(list, 'points').reverse(),
    9. };
    10. # leanpub-end-insert
    11. class App extends Component {
    12. ...
    13. }
    14. ...

    可以看到有两个排序函数返回一个反向列表。这是因为当用户首次点击排序的时候,希望查看评论和评分最高的项目,而不是最低的。

    现在,SORTS 对象允许你引用任何排序函数。

    我们的 App 组件负责存储排序函数的状态。组件的初始状态存储的是默认排序函数,它不对列表排序而只是将输入的list作为输出。

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

    1. this.state = {
    2. results: null,
    3. searchKey: '',
    4. searchTerm: DEFAULT_QUERY,
    5. error: null,
    6. isLoading: false,
    7. # leanpub-start-insert
    8. sortKey: 'NONE',
    9. # leanpub-end-insert
    10. };

    一旦用户选择了一个不同的sortKey,比如说 AUTHOR,App组件将从SORTS对象中选取合适的排序函数对列表进行排序。

    现在,我们要在App组件中定义一个新的类方法,用来将sortKey设置为App组件的状态。然后,sortKey 可以被用来选取对应的排序函数并对其列表进行排序。

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

    1. class App extends Component {
    2. constructor(props) {
    3. ...
    4. this.needsToSearchTopStories = this.needsToSearchTopStories.bind(this);
    5. this.setSearchTopStories = this.setSearchTopStories.bind(this);
    6. this.fetchSearchTopStories = this.fetchSearchTopStories.bind(this);
    7. this.onSearchSubmit = this.onSearchSubmit.bind(this);
    8. this.onSearchChange = this.onSearchChange.bind(this);
    9. this.onDismiss = this.onDismiss.bind(this);
    10. # leanpub-start-insert
    11. this.onSort = this.onSort.bind(this);
    12. # leanpub-end-insert
    13. }
    14. # leanpub-start-insert
    15. onSort(sortKey) {
    16. this.setState({ sortKey });
    17. }
    18. # leanpub-end-insert
    19. ...
    20. }

    下一步是将类方法和sortKey传递给 Table 组件。

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

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

    Table 组件负责对列表排序。它通过sortKey选取SORT对象中对应的排序函数,并列表作为该函数的输入。之后,Table 组件将在已排序的列表上继续 mapping。

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

    1. # leanpub-start-insert
    2. const Table = ({
    3. list,
    4. sortKey,
    5. onSort,
    6. onDismiss
    7. }) =>
    8. # leanpub-end-insert
    9. <div className="table">
    10. # leanpub-start-insert
    11. {SORTS[sortKey](list).map(item =>
    12. # leanpub-end-insert
    13. <div key={item.objectID} className="table-row">
    14. ...
    15. </div>
    16. )}
    17. </div>

    理论上,列表可以按照其中的任意排序函数进行排序,但是默认的排序 (sortKey) 是NONE,所以列表不进行排序。至此,还没有人执行onSort()方法来改变sortKey。让我们接下来用一行列标题来扩展表格,每个列标题会使用列中的 Sort 组件对每列进行排序。

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

    1. const Table = ({
    2. list,
    3. sortKey,
    4. onSort,
    5. onDismiss
    6. }) =>
    7. <div className="table">
    8. # leanpub-start-insert
    9. <div className="table-header">
    10. <span style={{ width: '40%' }}>
    11. <Sort
    12. sortKey={'TITLE'}
    13. onSort={onSort}
    14. >
    15. Title
    16. </Sort>
    17. </span>
    18. <span style={{ width: '30%' }}>
    19. <Sort
    20. sortKey={'AUTHOR'}
    21. onSort={onSort}
    22. >
    23. Author
    24. </Sort>
    25. </span>
    26. <span style={{ width: '10%' }}>
    27. <Sort
    28. sortKey={'COMMENTS'}
    29. onSort={onSort}
    30. >
    31. Comments
    32. </Sort>
    33. </span>
    34. <span style={{ width: '10%' }}>
    35. <Sort
    36. sortKey={'POINTS'}
    37. onSort={onSort}
    38. >
    39. Points
    40. </Sort>
    41. </span>
    42. <span style={{ width: '10%' }}>
    43. Archive
    44. </span>
    45. </div>
    46. # leanpub-end-insert
    47. {SORTS[sortKey](list).map(item =>
    48. ...
    49. )}
    50. </div>

    每个 Sort 组件都有一个指定的sortKey和通用的onSort()函数。Sort 组件调用onSort()方法去设置指定的sortKey

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

    1. const Sort = ({ sortKey, onSort, children }) =>
    2. <Button onClick={() => onSort(sortKey)}>
    3. {children}
    4. </Button>

    如你所见,Sort 组件重用了我们的 Button 组件,当点击按钮时,每个传入的sortKey都会被onSort()方法设置。现在,我们应该能够通过点击列标题来对列表进行排序了。

    这里有个改善外观的小建议。到目前为止,列标题中的按钮看起来有点傻。我们给 Sort 组件中的按钮添加一个合适的className

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

    1. const Sort = ({ sortKey, onSort, children }) =>
    2. # leanpub-start-insert
    3. <Button
    4. onClick={() => onSort(sortKey)}
    5. className="button-inline"
    6. >
    7. # leanpub-end-insert
    8. {children}
    9. </Button>

    现在应该看起来不错。接下来的目标是实现反向排序。如果点击 Sort 组件两次,该列表应该被反向排序。首先,我们需要用一个布尔值来定义反向状态 (isSortReverse)。排序可以反向或不反向。

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

    1. this.state = {
    2. results: null,
    3. searchKey: '',
    4. searchTerm: DEFAULT_QUERY,
    5. error: null,
    6. isLoading: false,
    7. sortKey: 'NONE',
    8. # leanpub-start-insert
    9. isSortReverse: false,
    10. # leanpub-end-insert
    11. };

    现在在排序方法中,可以评判列表是否被反向排序。如果状态中的 sortKey 与传入的 sortKey 相同,并且反向状态 (isSortReverse) 尚未设置为 true,则相反——反向状态 (isSortReverse) 设置为 true。

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

    1. onSort(sortKey) {
    2. # leanpub-start-insert
    3. const isSortReverse = this.state.sortKey === sortKey && !this.state.isSortReverse;
    4. this.setState({ sortKey, isSortReverse });
    5. # leanpub-end-insert
    6. }

    同样,将反向属性 (isSortReverse) 传递给 Table 组件。

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

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

    现在 Table 组件有一个块体箭头函数用于计算数据。

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

    1. # leanpub-start-insert
    2. const Table = ({
    3. list,
    4. sortKey,
    5. isSortReverse,
    6. onSort,
    7. onDismiss
    8. }) => {
    9. const sortedList = SORTS[sortKey](list);
    10. const reverseSortedList = isSortReverse
    11. ? sortedList.reverse()
    12. : sortedList;
    13. return(
    14. # leanpub-end-insert
    15. <div className="table">
    16. <div className="table-header">
    17. ...
    18. </div>
    19. # leanpub-start-insert
    20. {reverseSortedList.map(item =>
    21. # leanpub-end-insert
    22. ...
    23. )}
    24. </div>
    25. # leanpub-start-insert
    26. );
    27. }
    28. # leanpub-end-insert

    反向排序现在应该可以工作了。

    最后值得一提,为了改善用户体验,我们可以思考一个开放性的问题:用户可以区分当前是根据哪一列进行排序的吗?目前为止,用户是区别不出来的。我们可以给用户一个视觉反馈。

    每个 Sort 组件都已经有了其的特定sortKey。它可以用来识别被激活的排序。我们可以将内部组件状态sortKey作为激活排序标识 (activeSortKey) 传递给 Sort 组件。

    {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. <div className="table">
    14. <div className="table-header">
    15. <span style={{ width: '40%' }}>
    16. <Sort
    17. sortKey={'TITLE'}
    18. onSort={onSort}
    19. # leanpub-start-insert
    20. activeSortKey={sortKey}
    21. # leanpub-end-insert
    22. >
    23. Title
    24. </Sort>
    25. </span>
    26. <span style={{ width: '30%' }}>
    27. <Sort
    28. sortKey={'AUTHOR'}
    29. onSort={onSort}
    30. # leanpub-start-insert
    31. activeSortKey={sortKey}
    32. # leanpub-end-insert
    33. >
    34. Author
    35. </Sort>
    36. </span>
    37. <span style={{ width: '10%' }}>
    38. <Sort
    39. sortKey={'COMMENTS'}
    40. onSort={onSort}
    41. # leanpub-start-insert
    42. activeSortKey={sortKey}
    43. # leanpub-end-insert
    44. >
    45. Comments
    46. </Sort>
    47. </span>
    48. <span style={{ width: '10%' }}>
    49. <Sort
    50. sortKey={'POINTS'}
    51. onSort={onSort}
    52. # leanpub-start-insert
    53. activeSortKey={sortKey}
    54. # leanpub-end-insert
    55. >
    56. Points
    57. </Sort>
    58. </span>
    59. <span style={{ width: '10%' }}>
    60. Archive
    61. </span>
    62. </div>
    63. {reverseSortedList.map(item =>
    64. ...
    65. )}
    66. </div>
    67. );
    68. }

    现在在 Sort 组件中,我们可以基于sortKeyactiveSortKey得知排序是否被激活。给 Sort 组件增加一个className属性,用于在排序被激活的时候给用户一个视觉反馈。

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

    1. # leanpub-start-insert
    2. const Sort = ({
    3. sortKey,
    4. activeSortKey,
    5. onSort,
    6. children
    7. }) => {
    8. const sortClass = ['button-inline'];
    9. if (sortKey === activeSortKey) {
    10. sortClass.push('button-active');
    11. }
    12. return (
    13. <Button
    14. onClick={() => onSort(sortKey)}
    15. className={sortClass.join(' ')}
    16. >
    17. {children}
    18. </Button>
    19. );
    20. }
    21. # leanpub-end-insert

    这样定义sortClass的方法有点蠢,不是吗?有一个库可以让它看起来更优雅。首先,我们需要安装它。

    {title=”Command Line”,lang=”text”}

    1. npm install classnames

    其次,需要将其导入 src/App.js 文件。

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

    1. import React, { Component } from 'react';
    2. import fetch from 'isomorphic-fetch';
    3. import { sortBy } from 'lodash';
    4. # leanpub-start-insert
    5. import classNames from 'classnames';
    6. # leanpub-end-insert
    7. import './App.css';

    现在,我们可以通过条件式语句来定义组件的className

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

    1. const Sort = ({
    2. sortKey,
    3. activeSortKey,
    4. onSort,
    5. children
    6. }) => {
    7. # leanpub-start-insert
    8. const sortClass = classNames(
    9. 'button-inline',
    10. { 'button-active': sortKey === activeSortKey }
    11. );
    12. # leanpub-end-insert
    13. return (
    14. # leanpub-start-insert
    15. <Button
    16. onClick={() => onSort(sortKey)}
    17. className={sortClass}
    18. >
    19. # leanpub-end-insert
    20. {children}
    21. </Button>
    22. );
    23. }

    同样在运行测试时,我们会看到 Table 组件失败的快照测试,及一些失败的单元测试。由于我们再次更改了组件显示,因此可以选择接受快照测试。但是必须修复单元测试。在我们的 src/App.test.js文件中,需要为 Table 组件提供sortKeyisSortReverse

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

    1. ...
    2. describe('Table', () => {
    3. const props = {
    4. list: [
    5. { title: '1', author: '1', num_comments: 1, points: 2, objectID: 'y' },
    6. { title: '2', author: '2', num_comments: 1, points: 2, objectID: 'z' },
    7. ],
    8. # leanpub-start-insert
    9. sortKey: 'TITLE',
    10. isSortReverse: false,
    11. # leanpub-end-insert
    12. };
    13. ...
    14. });

    可能需要再一次接受 Table 组件的失败的快照测试,因为我们给 Table 组件提供更多的 props。

    现在,我们的高级排序交互完成了。

    练习:

    • 使用像Font Awesome这样的库来指示(反向)排序
      • 就是在每个排序标题旁边显示向上箭头或向下箭头图标
    • 阅读了解classnames

    {pagebreak}

    我们已经学会了React中的高级组件技术!现在来回顾一下本章:

    • React
      • 通过 ref 属性引用 DOM 节点
      • 高阶组件是构建高级组件的常用方法
      • 高级交互在 React 中的实现
      • 帮助实现条件 classNames 的一个优雅库
    • ES6

      • rest 解构拆分对象和数组

      你可以在官方代码库找到源代码。