高级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属性的情况。

练习

加载 ……

现在让我们回到应用程序。当向 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 解构拆分对象和数组

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