React Hooks 编程:useState和useEffect的详解

0 阅读5分钟

React Hooks 是 React 16.8 引入的一套全新 API,极大地提升了函数组件的能力。本文将以 useStateuseEffect 为核心,在简单demo实践中理解和使用Hooks。


useState:让函数组件拥有"记忆"

函数式编程的力量

在 JavaScript 中,函数是一等对象(First-class Object),意味着函数可以像变量一样被赋值、传递和返回。React 利用这一特性,将函数组件提升为主流。

我们就会思考以下两个问题:

  • 既然函数可以像对象一样被操作,能否让它"记住"某些状态?
  • 传统的函数每次执行都是"无记忆"的,如何让它有"记忆"?

useState 的魔法

useState 就是让函数组件拥有"记忆"的魔法。它的用法如下:

const [count, setCount] = useState(0);
  • count:当前状态值
  • setCount:更新状态的函数
  • 0:初始值

我们在前端开发服务器中,点击count is 0按钮count会自增,其用的就是useState带来的“魔法”。

image.png

所以,我们明白了:

  • 每次组件渲染,useState 都会返回当前的状态值。
  • 调用 setCount 会触发组件重新渲染,并用新值替换旧值。

函数也是类?原型式面向对象

在 JS 中,函数不仅是一等对象,还可以作为"类"来使用(通过原型链)。React 的函数组件本质上就是"无状态类",通过 Hooks 赋予其"状态"。

那么,请你思考一下:

  • 既然函数可以像类一样使用,能否让它拥有"实例属性"?
  • 传统的类组件有 this.state,函数组件如何实现类似功能?
  1. JavaScript 中的函数作为类

    function Person(name) {
      this.name = name;
    }
    Person.prototype.sayHello = function() {
      console.log(`Hello, I'm ${this.name}`);
    };
    
    const person = new Person("张三");
    person.sayHello(); // "Hello, I'm 张三"
    
  2. React 类组件 vs 函数组件

    // 类组件 - 有状态
    class Counter extends React.Component {
      constructor(props) {
        super(props);
        this.state = { count: 0 };
      }
      
      render() {
        return <div>{this.state.count}</div>;
      }
    }
    
    // 函数组件 - 原本无状态
    function Counter() {
      return <div>0</div>; // 无法保存状态
    }
    
  3. Hooks 让函数组件拥有"类"的能力

    // 使用 useState 后,函数组件拥有了"状态"
    function Counter() {
      const [count, setCount] = useState(0);
      return <div>{count}</div>; // 现在可以保存和更新状态了
    }
    

useState 实际上是在函数组件内部创建了一个"隐藏的状态存储",并且每次组件重新渲染时,React 会记住之前的状态值,这就像给函数组件添加了"实例属性",但实现方式更加优雅和函数式。


useEffect:副作用的管理者

什么是副作用(Side Effect)?

副作用指的是任何与组件渲染无关、但又需要在组件生命周期内执行的操作,比如:

  • 数据请求
  • 订阅/取消订阅
  • 手动操作 DOM
  • 定时器

useEffect 的基本用法

useEffect(() => {
  // 副作用逻辑
  return () => {
    // 清理逻辑
  };
}, [依赖项]);
  • 第一个参数是副作用函数
  • 返回值是清理函数(可选)
  • 第二个参数是依赖项数组

生命周期的映射

生命周期阶段useEffect 写法说明
挂载(mounted)useEffect(fn, [])只在初次渲染后执行一次
更新(updated)useEffect(fn, [依赖])依赖项变化时执行
卸载(unmounted)return () => { ... }组件卸载时执行清理逻辑

你再思考以下问题:

为什么要清理副作用?

答:比如定时器、订阅等,如果不清理会造成内存泄漏。而且请求数据时,组件卸载后响应式数据和 DOM 已经不存在,继续处理响应会报错或浪费资源。

代码示例

demo 项目中有如下代码:

import { useState, useEffect } from 'react';

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);

    // 清理副作用
    return () => clearInterval(timer);
  }, []);

  return <div>计时:{count} 秒</div>;
}

跟着我一起重复一遍:

组件挂载时启动定时器,每秒自增

组件卸载时清理定时器,防止内存泄漏


组件请求接口的最佳时机

何时请求数据?

  • 组件首次 渲染后(mounted) 是请求数据的最佳时机
  • 使用 useEffect(fn, []),只在初次渲染后执行一次

为什么不用依赖项?

  • 如果依赖项为空数组 [],组件状态改变不会重复请求
  • 只有在依赖项变化时才会重新执行副作用

请参考以下代码:

import { useState, useEffect } from 'react';

function App() {
  const [repos, setRepos] = useState([]);

  useEffect(() => {
    // api 数据 动态的
    console.log('只在组件挂载时运行一次!!');
    const fetchRepos = async () => {
      const response = await fetch('https://api.github.com/users/xxxx/repos');
      const data = await response.json();
      console.log(data);
      setRepos(data);
    }
    fetchRepos();
  }, []);

  return (
    <div>
      {/* 渲染仓库列表 */}
    </div>
  );
}

组件首次渲染时,useEffect 执行,发起 API 请求,依赖项为空数组 [],确保了只在组件挂载时执行一次。即使组件状态改变导致重新渲染,也不会重复请求数据,这是数据请求的最佳实践:只在需要时请求一次


useEffect 不能直接 async 的原因

❌ 错误写法:直接使用 async

// 错误!useEffect 不能直接是 async 函数
useEffect(async () => {
  const response = await fetch('https://api.github.com/users/LeeAt67/repos');
  const data = await response.json();
  setRepos(data);
}, []);

image.png

错误写法的报错提醒

✅ 正确写法:在内部声明 async 函数

useEffect(() => {
  async function fetchData() {
    // 异步请求
  }
  fetchData();
}, []);

为什么不能直接 async?

useEffect 期望返回一个清理函数(或 undefined),而 async 函数返回的是 Promise,这会导致 React 无法正确处理副作用的清理,这会导致控制台警告和潜在的清理逻辑失效


总结:让我们再次回顾一下!!!!

  • Hooks 让函数组件拥有了"记忆"和"副作用管理"能力
  • useState 负责状态,"记住"数据
  • useEffect 负责副作用和生命周期
  • 清理副作用是防止内存泄漏的关键
  • 请求数据应在组件首次渲染后进行
  • useEffect 内不能直接 async,需包裹一层