如何使用 React Hooks 重构类组件?(下)

简介: 如何使用 React Hooks 重构类组件?

如何使用 React Hooks 重构类组件?(上)https://developer.aliyun.com/article/1411375


5.  防止组件重新渲染


React 非常快,通常我们不必担心过早的优化。但是,在某些情况下,优化组件并确保它们不会过于频繁地重新渲染是很有必要的。

例如,减少类组件重新渲染的常用方法是使用 PureComponent 或者 shouldComponentUpdate 生命周期。下面例子中有两个类组件(父组件和子组件),父组件有两个状态值:counterfruit。子组件只在父组件的 fruit 发生变化时重新渲染。所以,使用 shouldComponentUpdate 生命周期来检查 fruit 属性是否改变。 如果相同,则子组件不会重新渲染。

父组件:

import { Component } from "react";
import PreventRerenderClass from "./PreventRerenderClass.jsx";
function randomInteger(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}
const fruits = ["banana", "orange", "apple", "kiwi", "mango"];
class PreventRerenderExample extends Component {
  state = {
    fruit: null,
    counter: 0,
  };
  pickFruit = () => {
    const fruitIdx = randomInteger(0, fruits.length - 1);
    const nextFruit = fruits[fruitIdx];
    this.setState({
      fruit: nextFruit,
    });
  };
  componentDidMount() {
    this.pickFruit();
  }
  render() {
    return (
      <div>
        <h3>
          Current fruit: {this.state.fruit} | counter: {this.state.counter}
        </h3>
        <button onClick={this.pickFruit}>挑一个水果</button>
        <button
          onClick={() =>
            this.setState(({ counter }) => ({
              counter: counter + 1,
            }))
          }
        >
          Increment
        </button>
        <button
          onClick={() =>
            this.setState(({ counter }) => ({ counter: counter - 1 }))
          }
        >
          Decrement
        </button>
        <div className="section">
          <PreventRerenderClass fruit={this.state.fruit} />
        </div>
      </div>
    );
  }
}
export default PreventRerenderExample;

子组件:

import { Component } from "react";
class PreventRerenderClass extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    return this.props.fruit !== nextProps.fruit;
  }
  render() {
    return (
      <div>
        <p>Fruit: {this.props.fruit}</p>
      </div>
    );
  }
}
export default PreventRerenderClass;

随着 hooks 的引入,我们得到了一个新的高阶组件,称为 memo。它可用于优化性能并防止函数组件重新渲染。下面来看看它是怎么用的。

父组件

import { useState, useEffect } from "react";
import PreventRerenderHooks from "./PreventRerenderHooks.jsx";
function randomInteger(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}
const fruits = ["banana", "orange", "apple", "kiwi", "mango"];
const PreventRerenderExample = () => {
  const [fruit, setFruit] = useState(null);
  const [counter, setCounter] = useState(0);
  const pickFruit = () => {
    const fruitIdx = randomInteger(0, fruits.length - 1);
    const nextFruit = fruits[fruitIdx];
    setFruit(nextFruit);
  };
  useEffect(() => {
    pickFruit();
  }, []);
  return (
    <div>
      <h3>
        Current fruit: {fruit} | counter: {counter}
      </h3>
      <button onClick={pickFruit}>挑一个水果</button>
      <button onClick={() => setCounter(counter => counter + 1)}>
        Increment
      </button>
      <button onClick={() => setCounter(counter => counter - 1)}>
        Decrement
      </button>
      <div className="section">
        <PreventRerenderHooks fruit={fruit} />
      </div>
    </div>
  );
};
export default PreventRerenderExample;

子组件:

import { memo } from "react";
const PreventRerenderHooks = props => {
  return (
    <div>
      <p>Fruit: {props.fruit}</p>
    </div>
  );
};
export default memo(PreventRerenderHooks);

PreventRerenderHooks 组件使用 memo 组件包装,并且仅在 props 中的 fruit 发生变化时发挥重新渲染。需要注意,memo组件执行的是浅比较,因此如果需要更好地控制memo组件何时重新渲染,可以提供自己的函数来执行 props 比较。

import { memo } from "react";
const PreventRerenderHooks = props => {
  return (
    <div>
      <p>Fruit: {props.fruit}</p>
    </div>
  );
};
export default memo(PreventRerenderHooks, (prevProps, nextProps) => {
  return prevProps.fruit !== nextProps.fruit
});


6. Context API


Context API 是一个很好用的工具,可以为组件层次结构中不同级别的组件提供值。 可以使用 React 提供的 createContext 方法创建新的上下文。先来看一个在类组件中使用 context 的例子。

Context Provider:

import { createContext } from "react";
export const UserContext = createContext();
export const UserActionsContext = createContext();

在父组件中,向消费者提供了 UserContextUserActionsContext

typescript

复制代码

import { Component, createContext } from "react";
import ContextApiClassConsumer from "./ContextApiClassConsumer.jsx";
import { UserContext, UserActionsContext } from "./userContext.js";
class ContextApiHooksProvider extends Component {
  state = {
    user: {
      name: "Class",
    },
  };
  setUser = user => this.setState({ user });
  render() {
    return (
      <UserContext.Provider value={this.state.user}>
        <UserActionsContext.Provider value={this.setUser}>
          <ContextApiClassConsumer />
        </UserActionsContext.Provider>
      </UserContext.Provider>
    );
  }
}
export default ContextApiHooksProvider;

这里 ContextApiClassConsumer 组件就可以获取到父组件提供的usersetUser

Context Consumer:

import { Component } from "react";
import { UserContext, UserActionsContext } from "./userContext.js";
class ContextApiClassConsumer extends Component {
  render() {
    return (
      <UserContext.Consumer>
        {user => (
          <UserActionsContext.Consumer>
            {setUser => (
              <div>
                <input
                  type="text"
                  value={user.name}
                  onChange={e =>
                    setUser({
                      name: e.target.value,
                    })
                  }
                />
              </div>
            )}
          </UserActionsContext.Consumer>
        )}
      </UserContext.Consumer>
    );
  }
}
export default ContextApiClassConsumer;

在上面的例子中,UserContext.Consumer 组件的子函数接收 user 状态,UserActionsContext.Consumer 的子函数接收 setUser 方法。

使用 Hooks 实现和上面的代码非常类似,但是会更简洁。同样,我们使用 UserContext.ProviderUserActionsContext.Provider 组件来提供 user 状态和 setUser 方法。

Context Provider:

import { useState } from "react";
import ContextApiHooksConsumer from "./ContextApiHooksConsumer.jsx";
import { UserContext, UserActionsContext } from "./userContext.js";
const ContextApiHooksProvider = () => {
  const [user, setUser] = useState({
    name: "Hooks",
  });
  return (
    <UserContext.Provider value={user}>
      <UserActionsContext.Provider value={setUser}>
        <ContextApiHooksConsumer />
      </UserActionsContext.Provider>
    </UserContext.Provider>
  );
};
export default ContextApiHooksProvider;

在函数组件中,我们可以像在类组件中一样使用 context,但是,hooks 中有一种更简洁的方法,我们可以利用 useContext hook 来访问 context 值。

Context Consumer:

import { useContext } from "react";
import { UserContext, UserActionsContext } from "./userContext.js";
const ContextApiHooksConsumer = () => {
  const user = useContext(UserContext);
  const setUser = useContext(UserActionsContext);
  return (
    <div>
      <input
        type="text"
        value={user.name}
        onChange={e =>
          setUser({
            name: e.target.value,
          })
        }
      />
    </div>
  );
};
export default ContextApiHooksConsumer;


7. 跨重新渲染保留值


在某些情况下,我们可能需要再组件中存储一些数据。但是不希望将其存储在状态中,因为 UI 不以任何方式依赖这些数据。

例如,我们可能会保存一些希望稍后包含在 API 请求中的元数据。这在类组件中很容易实现,只需为类分配一个新属性即可。


import { Component } from "react";
class PreservingValuesClass extends Component {
  state = {
    counter: 0,
  };
  componentDidMount() {
    this.valueToPreserve = Math.random();
  }
  showValue = () => {
    alert(this.valueToPreserve);
  };
  increment = () => this.setState(({ counter }) => ({ counter: counter + 1 }));
  render() {
    return (
      <div>
        <p>Counter: {this.state.counter}</p>
        <button onClick={this.increment}>Increment</button>
        <button onClick={this.showValue}>Show</button>
      </div>
    );
  }
}
export default PreservingValuesClass;

在这个例子中,当组件被挂载时,我们在 valueToPreserve 属性上分配了一个动态随机数。除此之外,还有 increment 方法来强制重新渲染,但是Show按钮时会弹窗显示保留的值。

这在类组件中很容易实现,但是在函数组件中就没那么简单了。这是因为,任何时候函数组件的重新渲染都会导致函数中的所有内容重新执行。这意味着如果我们有这样的组件:

const MyComponent = props => {
  const valueToPreserve = Math.random()
  // ...
}

组件每次重新渲染时都会重新调用 Math.random() 方法,因此创建的第一个值将丢失。

避免此问题的一种方法是将变量移到组件之外。 但是,这是行不通的,因为如果该组件被多次使用,则该值会将被它们中的每一个覆盖。

恰好,React 提供了一个非常适合这个用例的 hook。 我们可以通过使用 useRef hook 来保留函数组件中重新渲染的值。


import { useState, useRef, useEffect } from "react";
const PreserveValuesHooks = props => {
  const valueToPreserve = useRef(null);
  const [counter, setCounter] = useState(0);
  const increment = () => setCounter(counter => counter + 1);
  const showValue = () => {
    alert(valueToPreserve.current);
  };
  useEffect(() => {
    valueToPreserve.current = Math.random();
  }, []);
  return (
    <div>
      <p>Counter: {counter}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={showValue}>Show value</button>
    </div>
  );
};
export default PreserveValuesHooks;

valueToPreserve 是一个初始值为 nullref。 但是,它后来在 useEffect 中更改为我们想要保留的随机数。


8. 如何向父组件传递状态和方法?


尽管我们不应该经常访问子组件的状态和属性,但是在某些情况下它可能会很有用。例如,我们想要重置某些组件的状态或者访问它的状态。我们需要创建一个 Ref,可以在其中存储对想要访问的子组件的引用。在类组件中,可以使用 createRef 方法,然后将该 ref 传递给子组件。

父组件:

import { Component, createRef } from "react";
import ExposePropertiesClassChild from "./ExposePropertiessClassChild";
class ExposePropertiesClassParent extends Component {
  constructor(props) {
    super(props);
    this.childRef = createRef();
  }
  showValues = () => {
    const counter = this.childRef.current.state.counter;
    const multipliedCounter = this.childRef.current.getMultipliedCounter();
    alert(`
      counter: ${counter}
      multipliedCounter: ${multipliedCounter}
    `);
  };
  increment = () => this.setState(({ counter }) => ({ counter: counter + 1 }));
  render() {
    return (
      <div>
        <button onClick={this.showValues}>Show</button>
        <ExposePropertiesClassChild ref={this.childRef} />
      </div>
    );
  }
}
export default ExposePropertiesClassParent;

子组件:

import { Component } from "react";
class ExposePropertiesClassChild extends Component {
  state = {
    counter: 0,
  };
  getMultipliedCounter = () => {
    return this.state.counter * 2;
  };
  increment = () => this.setState(({ counter }) => ({ counter: counter + 1 }));
  render() {
    return (
      <div>
        <p>Counter: {this.state.counter}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}
export default ExposePropertiesClassChild;

要访问子组件的属性,只需要在父组件中创建一个 ref 并传递它。 现在,让我们看看如何使用函数组件和 hook 来实现相同的目标。

父组件:

import { useRef } from "react";
import ExposePropertiesHooksChild from "./ExposePropertiesHooksChild";
const ExposePropertiesHooksParent = props => {
  const childRef = useRef(null);
  const showValues = () => {
    const counter = childRef.current.counter;
    const multipliedCounter = childRef.current.getMultipliedCounter();
    alert(`
      counter: ${counter}
      multipliedCounter: ${multipliedCounter}
    `);
  };
  return (
    <div>
      <button onClick={showValues}>Show child values</button>
      <ExposePropertiesHooksChild ref={childRef} />
    </div>
  );
};
export default ExposePropertiesHooksParent;

在父组件中,我们使用 useRef hook 来存储对子组件的引用。 然后在 showValues 函数中访问 childRef 的值。 可以看到,这里与类组件中的实现非常相似。

子组件:

import { useState, useImperativeHandle, forwardRef } from "react";
const ExposePropertiesHooksChild = (props, ref) => {
  const [counter, setCounter] = useState(0);
  const increment = () => setCounter(counter => counter + 1);
  useImperativeHandle(ref, () => {
    return {
      counter,
      getMultipliedCounter: () => counter * 2,
    };
  });
  return (
    <div>
      <p>Counter: {counter}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};
export default forwardRef(ExposePropertiesHooksChild);

forwardRef 将从父组件传递的 ref 转发到组件,而 useImperativeHandle 指定了父组件应该可以访问的内容。


9. 小结


通过这篇文章,相信你对使用Hooks(函数组件)来重构类组件有了一定了解。Hooks 的出现使得 React 代码更加简洁,并且带来了更好的状态逻辑可重用性。在开始编写 Hooks 之前,建议先阅读 React Hooks 的官方文档,因为在编写时需要遵循某些规则,例如不要改变 hooks 的调用顺序。



相关文章
|
1天前
|
前端开发 JavaScript 开发者
React 按钮组件 Button
本文介绍了 React 中按钮组件的基础概念,包括基本的 `&lt;button&gt;` 元素和自定义组件。详细探讨了事件处理、参数传递、状态管理、样式设置和可访问性优化等常见问题及其解决方案,并提供了代码示例。帮助开发者避免易错点,提升按钮组件的使用体验。
99 77
|
2天前
|
前端开发 UED 开发者
React 对话框组件 Dialog
本文详细介绍了如何在 React 中实现一个功能完备的对话框组件(Dialog),包括基本用法、常见问题及其解决方案,并通过代码案例进行说明。从安装依赖到创建组件、添加样式,再到解决关闭按钮失效、背景点击无效、键盘导航等问题,最后还介绍了如何添加动画效果和处理异步关闭操作。希望本文能帮助你在实际开发中更高效地使用 React 对话框组件。
96 75
|
7天前
|
前端开发 Java API
React 进度条组件 ProgressBar 详解
本文介绍了如何在 React 中创建进度条组件,从基础实现到常见问题及解决方案,包括动态更新、状态管理、性能优化、高级动画效果和响应式设计等方面,帮助开发者构建高效且用户体验良好的进度条。
35 18
|
22天前
|
存储 前端开发 测试技术
React组件的最佳实践
React组件的最佳实践
|
20天前
|
前端开发 API 开发者
React 文件上传组件 File Upload
本文详细介绍了如何在 React 中实现文件上传组件,从基础的文件选择和上传到服务器,再到解决文件大小、类型限制、并发上传等问题,以及实现多文件上传、断点续传和文件预览等高级功能,帮助开发者高效构建可靠的应用。
48 12
|
15天前
|
存储 前端开发 JavaScript
React 表单输入组件 Input:常见问题、易错点及解决方案
本文介绍了在 React 中使用表单输入组件 `Input` 的基础概念,包括受控组件与非受控组件的区别及其优势。通过具体代码案例,详细探讨了创建受控组件、处理多个输入字段、输入验证和格式化的方法,并指出了常见易错点及避免方法,旨在提升表单的健壮性和用户体验。
27 4
|
22天前
|
前端开发 JavaScript API
React 文件下载组件 File Download
本文介绍了在React中实现文件下载组件的方法,包括使用`a`标签和JavaScript动态生成文件,解决了文件路径、文件类型、大文件下载及文件名乱码等问题,并展示了使用第三方库`file-saver`和生成CSV文件的高级用法。
35 6
|
19天前
|
前端开发 JavaScript API
React 文件下载组件:File Download
本文详细介绍了如何在React应用中实现文件下载组件,包括基本概念、实现步骤和代码示例。同时,探讨了常见问题如文件类型不匹配、文件名乱码等及其解决方法,旨在提升用户体验和代码可维护性。
38 2
|
23天前
|
存储 前端开发 JavaScript
React 文件上传组件 File Upload
本文介绍了如何在 React 中实现文件上传组件,包括基本的概念、实现步骤、常见问题及解决方案。通过 `&lt;input type=&quot;file&quot;&gt;` 元素选择文件,使用 `fetch` 发送请求,处理文件类型和大小限制,以及多文件上传和进度条显示等高级功能,帮助开发者构建高效、可靠的文件上传组件。
65 2
|
24天前
|
存储 前端开发
在React框架中,如何使用对象来管理组件的状态
在React中,组件状态通过`state`对象管理,利用`setState`方法更新状态。状态变化触发组件重新渲染,实现UI动态更新。对象结构清晰,便于复杂状态管理。