React Hooks

文章目录

  • React Hooks概述
    • 由来
    • 基本语法
    • Hooks的规则
  • 构建自己的hooks
    • 自定义Hooks
    • 在 Hooks 之间传递信息
  • 代码对比
  • Hooks API
  • 参考链接

React Hooks概述

由来

  1. Hooks 允许您在不更改组件层次结构的情况下重用有状态逻辑
  2. Hooks允许您根据相关内容(例如设置订阅或获取数据)将一个组件拆分为较小的函数,而不是基于生命周期方法强制拆分。您还可以选择使用 reducer 管理组件的本地state(状态),以使其更具可预测性。
  3. Hooks 允许您在没有类的情况下使用更多 React 的功能

基本语法

  1. const [count, setCount] = useState(0);
    • count 相当于 之前的 state.count
      当我们想要在类中显示当前计数时,我们使用 this.state.count 读取:

      You clicked {this.state.count} times

      在函数中,我们直接使用 count 读取:

      You clicked {count} times

    • setCount 相当于 this.setState(count)
      在类中,我们需要调用 this.setState() 来更新 count 状态::
      
      
      在函数中,我们已经将 setCountcount 作为变量,因此我们不需要 this :
      
      
    • useState(0) 相当于 状态以 { count: 0 } 开始
  2. 使用多个 state(状态) 变量
    将 state(状态) 变量 声明为一对[something, setSomething] 也很方便,因为如果我们想使用多个状态变量,它可以为 不同 的 state(状态) 变量赋予不同的名称:
function ExampleWithManyStates() {
  // Declare multiple state variables!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  1. Effect Hook
    可以将 useEffect Hook 视为 componentDidMount,componentDidUpdate 和 componentWillUnmount的组合。
 useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

提示:
与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调用的 effects 不会阻止浏览器更新屏幕。这让你的应用程序感觉更有响应性。大多数 effects 不需要同步发生。在一些不常见的情况下(比如测量布局),有一个单独的 useLayoutEffect Hook,其 API 与 useEffect 相同。

  • 需要清理的 side effects:
    有些 effects 需要清理。 例如,我们可能希望设置对某些外部数据源的订阅。 在这种情况下,清理是非常重要的,这样我们就不会引起内存泄漏!
    在React类中,通常会在** componentDidMount 中设置订阅**,然后在 componentWillUnmount 中清理订阅。例如:对定时器的设置和清除

    • 在 classes(类) 中的例子:
     componentDidMount() {
        this.timer = setTimeout(() => {
          nextTick();
        }, 2000);
      }
    
      componentWillUnmount() {
         clearTimeout(this.timer);
      }
    
    • 使用Hooks的例子:
    useEffect(() => {
        const timer = setTimeout(() => {
          nextTick();
        }, 2000);
        return () => {
          clearTimeout(timer);  // Clean up the subscription
        };
      });
      
    

    这是 effect 的可选清除机制。每个 effect 都可能返回一个在它之后进行清理的函数。这让我们可以将添加和删除订阅的逻辑紧密地保持在一起。它们是相同 effect 的一部分。

  • 使用多个 Effects 来分离关注点:

    • 在 classes(类) 中,注意设置 document.title 的逻辑如何在 componentDidMount 和 componentDidUpdate 之间拆分。订阅逻辑还分布在 componentDidMount 和 componentWillUnmount 之间。componentDidMount 包含两个任务的代码。例如:
    class FriendStatusWithCounter extends React.Component {
      constructor(props) {
        super(props);
        this.state = { count: 0, isOnline: null };
        this.handleStatusChange = this.handleStatusChange.bind(this);
      }
    
      componentDidMount() {
        document.title = `You clicked ${this.state.count} times`;
        ChatAPI.subscribeToFriendStatus(
          this.props.friend.id,
          this.handleStatusChange
        );
      }
    
      componentDidUpdate() {
        document.title = `You clicked ${this.state.count} times`;
      }
    
      componentWillUnmount() {
        ChatAPI.unsubscribeFromFriendStatus(
          this.props.friend.id,
          this.handleStatusChange
        );
      }
    
      handleStatusChange(status) {
        this.setState({
          isOnline: status.isOnline
        });
    }
    
    • Hooks 如何解决这个问题呢?就像你可以多次使用 State Hook 一样,你也可以使用多个 effects 。这样,我们将不相关的逻辑分成不同的 effects
    function FriendStatusWithCounter(props) {
      const [count, setCount] = useState(0);
      useEffect(() => {
        document.title = `You clicked ${count} times`;
      });
    
      const [isOnline, setIsOnline] = useState(null);
      useEffect(() => {
        ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
        return () => {
          ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
        };
      });
    
      function handleStatusChange(status) {
        setIsOnline(status.isOnline);
      }
      // ...
    }
    
  • 通过跳过 Effects 来优化性能

    • 在某些情况下,在每次渲染后清理或应用 effect 可能会产生性能问题。在类组件中,我们可以通过在componentDidUpdate 中编写与 prevPropsprevState 的额外比较来解决这个问题:
    componentDidUpdate(prevProps, prevState) {
      if (prevState.count !== this.state.count) {
        document.title = `You clicked ${this.state.count} times`;
      }
    }
    
    • 这个要求很常见,它被内置到 useEffect Hook API中。如果在重新渲染之间没有更改某些值,则可以告诉React 跳过应用 effect 。为此,将数组作为可选的第二个参数传递给 useEffect
    useEffect(() => {
      document.title = `You clicked ${count} times`;
    }, [count]); // Only re-run the effect if count changes
    

    在上面的例子中,我们传递 [count] 作为第二个参数。如果count不发生改变,则React 会跳过这个 effect ,这是我们的优化。当count发生改变时,就算数组中有多个项目,React 也将重新运行 effect ,即使其中只有一个不同。

    这也适用于具有清理阶段的 effect :

    useEffect(() => {
      ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
      return () => {
        ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
      };
    }, [props.friend.id]); // Only re-subscribe if props.friend.id changes
    

    将来, 第二个参数可能会通过构建时转换自动添加。

    注意
    如果使用此优化,请确保该数组包含外部作用域中随时间变化且 effect 使用的任何值。 否则,您的代码将引用先前渲染中的旧值。 我们还将在 Hooks API 参考中讨论其他的优化选项。
    如果要运行 effect 并仅将其清理一次(在装载和卸载时),则可以将空数组([])作为第二个参数传递。 这告诉React你的 effect 不依赖于来自 props 或 state 的任何值,所以它永远不需要重新运行。这不作为特殊情况处理 - 它直接遵循输入数组的工作方式。虽然传递 [] 更接近熟悉的 componentDidMount 和componentWillUnmount 心智模型,但我们建议不要将它作为一种习惯,因为它经常会导致错误,如上所述 。 不要忘记 React 推迟运行 useEffect 直到浏览器绘制完成后,所以做额外的工作不是问题。

Hooks的规则

Hooks 是 JavaScript 函数,但在使用它们时需要遵循两个规则。 我们提供了一个 linter 插件 来自动执行这些规则:

  1. 只在顶层调用Hook

    不要在循环,条件或嵌套函数中调用 Hook 。相反,总是在 React 函数的顶层使用 Hooks。通过遵循此规则,您可以确保每次组件渲染时都以相同的顺序调用 Hook 。 这就是允许 React 在多个 useState 和 useEffect 调用之间能正确保留 Hook 状态的原因。

    React 依赖于调用 Hooks 的顺序 。只要 Hook 调用的顺序在每次渲染之间是相同的,React 就可以将一些本地 state(状态) 与每次渲染相关联。例如:

    // ------------
    // 第一次渲染
    // ------------
    useState('Mary')           // 1. 用'Mary'初始化名称状态变量
    useEffect(persistForm)     // 2. 添加一个 effect 用于持久化form
    useState('Poppins')        // 3. 使用 'Poppins' 初始化 surname 状态变量
    useEffect(updateTitle)     // 4. 添加一个 effect 用于更新 title
    
    // -------------
    // 第二次渲染
    // -------------
    useState('Mary')           // 1. 读取 name  状态变量(忽略参数)
    useEffect(persistForm)     // 2. 替换 effect 以持久化 form
    useState('Poppins')        // 3. 读取 surname 状态变量(忽略参数)
    useEffect(updateTitle)     // 4. 替换 effect 用于更新 title
    
    // ...
    

    但是如果我们在条件中放置 Hook 调用(例如,persistForm effect)会发生什么呢?

     // 我们在条件语句中使用Hook,打破了第一条规则
      if (name !== '') {
        useEffect(function persistForm() {
          localStorage.setItem('formData', name);
        });
      }
    

    name !== ‘’ 条件在第一次渲染时为 true ,因此我们运行此 Hook 。 但是,在下一次渲染时,用户可能会清除form,使条件置为 false 。 现在我们在渲染过程中跳过此 Hook ,Hook 调用的顺序变得不同:

    useState('Mary')           // 1. 读取 name  状态变量(忽略参数)
    // useEffect(persistForm)  //  这个Hook被跳过了
    useState('Poppins')        // 2 (但是之前是 3). 读取 surname 状态变量失败
    useEffect(updateTitle)     // 3 (但是之前是 4). 替换 effect 失败 
    

    React 不知道第二次 useState Hook 调用返回什么。React 期望这个组件中的第二个 Hook 调用对应于 persistForm effect,就像之前的渲染一样,但现在已经不存在了。从那时起,在我们跳过的那个 Hook 调用之后的每一个 Hook 调用也会移动一个,从而导致 bug。

    这就是为什么我们需要在组件顶层调用 Hook 的原因。 如果我们想要有条件地运行一个效果,我们可以把这个条件 放置 在我们的 Hook 中:

    useEffect(function persistForm() {
        // ? 我们不再违反第一条规则了
        if (name !== '') {
          localStorage.setItem('formData', name);
        }
      });
    

    请注意,如果使用 我们提供的lint规则 的话,就不需要担心这个问题

  2. 只在 React Functions 调用 Hooks

    不要在常规 JavaScript 函数中调用 Hook 。 相反,你可以:

    • React 函数式组件中调用 Hooks 。
    • 从自定义 Hooks 调用 Hooks (我们将在下一页中学习 自定义 Hooks )。

    通过遵循此规则,您可以确保组件中的所有 stateful (有状态)逻辑在其源代码中清晰可见。

构建自己的hooks

自定义Hooks

构建自己的 Hooks 可以将组件逻辑提取到可重用的函数中,可以使组件和逻辑分离。

  1. 有一个FriendStatus组件,用于在聊天应用程序中,显示一条消息,指示朋友是否在线:

    import { useState, useEffect } from 'react';
    
    function FriendStatus(props) {
      const [isOnline, setIsOnline] = useState(null);
    
      function handleStatusChange(status) {
        setIsOnline(status.isOnline);
      }
    
      useEffect(() => {
        ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
        return () => {
          ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
        };
      });
    
      if (isOnline === null) {
        return 'Loading...';
      }
      return isOnline ? 'Online' : 'Offline';
    }
    
  2. 还有一个FriendListItem 组件,用于在聊天应用程序的应用程序的联系人列表中,将在线用户的用户名显示为绿色。

    import { useState, useEffect } from 'react';
    
    function FriendListItem(props) {
      const [isOnline, setIsOnline] = useState(null);
    
      function handleStatusChange(status) {
        setIsOnline(status.isOnline);
      }
    
      useEffect(() => {
        ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
        return () => {
          ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
        };
      });
    
      return (
        
  3. {props.friend.name}
  4. ); }

    我们想在 FriendStatus 和 FriendListItem 之间分享这个逻辑。传统上,在 React 中,我们有两种流行的方式来共享组件之间的状态逻辑:render props(渲染属性)higher-order components(高阶组件)

我们现在将看看 Hook 如何在不强制您向树中添加更多组件的情况下解决许多相同的问题。

  1. 提取自定义Hook

    当我们想要在两个 JavaScript 函数之间共享逻辑时,我们会将共享逻辑提取到第三个函数自定义 Hook 是一个 JavaScript 函数,其名称以 use 开头,可以调用其他 Hook。 例如,下面的 useFriendStatus 是我们的第一个自定义 Hook ,目的是订阅一个朋友的状态 :

    import { useState, useEffect } from 'react';
    
    function useFriendStatus(friendID) {
      const [isOnline, setIsOnline] = useState(null);
    
      function handleStatusChange(status) {
        setIsOnline(status.isOnline);
      }
    
      useEffect(() => {
        ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
        return () => {
          ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
        };
      });
    
      return isOnline;
    }
    
  2. 这样我们可以分别在FriendStatus组件FriendListItem 组件中使用它:

    import useFriendStatus from "./useFriendStatus";
    
    function FriendStatus(props) {
      const isOnline = useFriendStatus(props.friend.id);
    
      if (isOnline === null) {
        return 'Loading...';
      }
      return isOnline ? 'Online' : 'Offline';
    }
    
    import useFriendStatus from "./useFriendStatus";
    
    function FriendListItem(props) {
      const isOnline = useFriendStatus(props.friend.id);
    
      return (
        
  3. {props.friend.name}
  4. ); }

    这种写法完全等价于原始的写法,自定义 Hooks 是一种惯例,它自然地遵循 Hooks 设计的约定,而不是 React 特性。

注意:
两个组件使用相同的 Hook 共享 state(状态) 吗? 不会。
自定义 Hooks 是一种重用 stateful(有状态) 逻辑 的机制(例如设置订阅和记住当前值),但是每次使用自定义 Hook 时,它内部的所有状态和效果都是完全隔离的

每次对 Hook 的调用都会被隔离。因为我们直接调用 useFriendStatus ,从 React 的角度来看,我们的组件只调用 useStateuseEffect 。正如我们 之前 所学到的 的,我们可以在一个组件中多次调用 useState 和 useEffect ,它们将完全独立的。

在 Hooks 之间传递信息

由于 Hooks 是函数,所以我们可以在它们之间传递信息。

假如我们现在有一个ChatRecipientPicker组件,这是一个聊天消息收件人选择器,显示当前选择的朋友是否在线:

const friendList = [
  { id: 1, name: 'Phoebe' },
  { id: 2, name: 'Rachel' },
  { id: 3, name: 'Ross' },
];

function ChatRecipientPicker() {
  const [recipientID, setRecipientID] = useState(1);
  const isRecipientOnline = useFriendStatus(recipientID);

  return (
    <>
      
      
    
  );
}

我们将当前选择的 friend ID 保存在 recipientID state(状态) 变量中,如果用户在 选择器中选择了不同的好友,则更新它。

因为useState Hook 调用提供给我们 recipientID state(状态)变量的最新值,所以我们可以将它作为参数传递给自定义的 useFriendStatus Hook。这让我们知道当前选择的朋友是否在线。如果我们选择一个不同的朋友并更新 recipientID state(状态)变量,我们的 useFriendStatus Hook 将取消订阅以前选择的朋友,并订阅新选择的朋友的状态。

代码对比

我们现在写一个Add todo功能,代码如下 :

原始写法:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      todos: []
    };
    this.deleteTodo = this.deleteTodo.bind(this);
    this.addTodo = this.addTodo.bind(this);
  }

  componentDidMount() {
    console.log(`You have ${this.state.todos.length} todos`);
  }

  componentDidUpdate() {
    console.log(`You have ${this.state.todos.length} todos`);
  }

  addTodo(todoText) {
    this.setState({
      todos: [...this.state.todos, todoText]
    });
  }

  deleteTodo(todoIndex) {
    const newTodos = this.state.todos.filter((_, index) => todoIndex !== index);
    this.setState({
      todos: newTodos
    });
  }

  render() {
    return (
      
Todos { const trimmedText = todoText.trim(); if (trimmedText.length > 0) { this.addTodo(trimmedText); } }} />
); } } const rootElement = document.getElementById("root"); ReactDOM.render(, rootElement);

Hooks写法:

import { useState, useEffect } from 'react';

const App = () => {
  const [todos, setTodo] = useState([]);

    useEffect(() => {
      console.log(`You have ${todos.length} todos`);
  });

  const addTodo = (todoText) => {
      setTodo([...todos, todoText]);
  }

  const deleteTodo = (todoIndex) => {
    const newTodos = todos.filter((_, index) => todoIndex !== index);
       setTodo(newTodos);
  }
  return (
    
Todos { const trimmedText = todoText.trim(); if (trimmedText.length > 0) { addTodo(trimmedText); } }} />
); } const rootElement = document.getElementById("root"); ReactDOM.render(, rootElement);

组件与逻辑分开的Hooks写法:

  1. index.js
import TodoForm from "./TodoForm";
import TodoList from "./TodoList";
import useTodoState from "./useTodoState";

const App = () => {
  const { todos, addTodo, deleteTodo } = useTodoState([]);

  return (
    
Todos { const trimmedText = todoText.trim(); if (trimmedText.length > 0) { addTodo(trimmedText); } }} />
); }; const rootElement = document.getElementById("root"); ReactDOM.render(, rootElement);
  1. useTodoState.js
import { useState, useEffect } from "react";

export default initialValue => {
  const [todos, setTodos] = useState(initialValue);

  useEffect(() => {
    // Update the document title using the browser API
    console.log(`You have ${todos.length} todos`);
  });

  return {
    todos,
    addTodo: todoText => {
      setTodos([...todos, todoText]);
    },
    deleteTodo: todoIndex => {
      const newTodos = todos.filter((_, index) => index !== todoIndex);

      setTodos(newTodos);
    }
  };
};

  1. TodoForm.js
import useInputState from "./useInputState";

const TodoForm = ({ saveTodo }) => {
  const { value, reset, onChange } = useInputState("");

  return (
    
{ event.preventDefault(); saveTodo(value); reset(); }} > ); }; export default TodoForm;
  1. useInputState.js
import { useState } from "react";

export default initialValue => {
  const [value, setValue] = useState(initialValue);

  return {
    value,
    onChange: event => {
      setValue(event.target.value);
    },
    reset: () => setValue("")
  };
};

Hooks API

  • Basic Hooks
    • useState
    • useEffect
    • useContext
  • Additional Hooks
    • useReducer
    • useCallback
    • useMemo
    • useRef
    • useImperativeMethods
    • useLayoutEffect

参考链接

  • react官方文档

你可能感兴趣的:(React)