react(四)

目录

useReducer

useReducer 是一个用于管理复杂状态逻辑的 Hook,可以看作是 useState 的进阶版。当你的状态更新逻辑涉及多个子值,或者下一次状态依赖于上一次状态时,它能让代码更加清晰、可预测。

工作机制:

它的核心思想是将状态如何更新状态的逻辑分离开:

  • 状态 (state):当前的数据。
  • 动作 (action):一个描述"发生了什么"的普通对象,通常有一个 type 字段。
  • 分发函数 (dispatch):触发状态更新的方法,你把 action 传给它。
  • 归约函数 (reducer):一个纯函数,它接收当前的 state 和 action,然后计算并返回一个全新的 state。

工作流程:

基本使用:

  1. 导入 useReducer。
  2. 定义 reducer 函数:使用 switch 语句根据 action.type 来返回新的状态。
  3. 定义 initialState:状态的初始值。
  4. 在组件中调用 useReducer:它返回一个数组,包含当前状态(state)和一个分发函数(dispatch)。

代码:计数器功能

js 复制代码
import { useReducer } from 'react';

type State = {
  count: number;
}

type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset' }
  
// 1. 定义初始状态
const initialState: State = { count: 0 }

// 2. 定义 reducer 函数,根据 action 的类型更新状态
const reducer = (state: State, action: Action): State => { 
  switch (action.type) { 
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      return state;
  }
}

export const UseReducer = () => {
  // 3. 使用 useReducer,传入 reducer 和初始状态,得到当前状态和 dispatch 函数
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    <div>
      <h2>useReducer 示例</h2>
      <p>计数: {state.count}</p>
      {/* 通过调用 dispatch 并传入 action 对象来触发状态更新 */}
      <button onClick={ () => dispatch({type: 'increment'})}>+</button>
      <button onClick={ () => dispatch({type: 'decrement'})}>-</button>
      <button onClick={ () => dispatch({type: 'reset'})}>重置</button>
    </div>
  )
}

useReducer和 useState

useReducer和 useState都是 React 中用于管理组件状态的 Hook,但它们有不同的适用场景和使用方式。

useState

  • 简单、独立的状态管理
  • 基于函数更新:setState(newValue)或 setState(prev => newValue)
  • 适合的场景:
    • 布尔值开关(如 loading、modal 显示)
    • 简单的计数器
    • 表单输入值
    • 单个数据项

useReducer

  • 复杂状态逻辑管理
  • 基于动作(action):通过 dispatch(action)触发状态更新
  • 适合的场景:
    • 状态之间有复杂依赖关系
    • 状态逻辑复杂,需要提取复用
    • 状态结构复杂(如嵌套对象、数组)
    • 需要追溯状态变化历史

如何选择?

使用 useState 的情况:

  • 状态是独立的基本类型
  • 状态逻辑简单
  • 不需要复用状态逻辑
  • 组件内部状态,不涉及全局共享

使用 useReducer 的情况:

  • 状态之间有复杂依赖
  • 状态结构复杂(多个子值)
  • 需要可预测的状态变化
  • 状态逻辑需要复用或测试
  • 下一个状态依赖前一个状态
  • 需要实现撤销/重做功能

useContext

useContext是 React 提供的一个 Hook,用于在函数组件中轻松消费 React 上下文(Context),解决组件间数据传递的问题,避免了层层传递 props 的繁琐(称为 "prop drilling")。

什么是prop drilling

当父组件要传递数据给重孙组件时,要层层传递:父组件 -> 子组件 -> 孙组件 -> 重孙组件

useContext语法

  1. 创建:创建一个盒子(存放数据的地方)
js 复制代码
const MyData = createContext('默认值');
  1. 包裹:把数据放到盒子里
js 复制代码
// 在父组件使用 Provider 提供数据
function father() {
  const someData = "要传递的数据";
  
  return (
  	{/* 用 MyData.Provider 包裹,把要传递的数据放到 value属性 */}
    <MyData.Provider value={someData}>
      <child />
    </MyData.Provider>
  );
}
  1. 使用:在用到数据的地方拿出来使用
js 复制代码
// 在任何子组件中获取
function grandson() {
  const receivedData = useContext(MyData);
  return <div>{receivedData }</div>;
}

使用

ThemeContext.ts

js 复制代码
// 引入createContext 
import { createContext } from "react";
// 创建一个主题上下文,包含当前主题和切换主题的函数。提供默认值
export const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {},
});

Parent.ts

js 复制代码
import { Child } from "./Child";
import { useState } from "react";
// 引入主题上下文
import { ThemeContext } from "./ThemeContext";

export const Parent = () => {
  const [theme, setTheme] = useState('light');
  // 切换主题的函数
  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light');
  }
  return (
  		// 使用 ThemeContext.Provider 包裹子组件,并传入当前主题和切换主题的函数
    <ThemeContext.Provider value={{ theme, toggleTheme}}>
      <Child  />
      {/* 在父组件中添加一个按钮,点击后切换主题 */}
      <button onClick={toggleTheme}>
        切换主题
      </button>
    </ThemeContext.Provider>
  );
}

Child.ts

跟useContext操作没啥关系,起一个承上启下的引用作用,只是为了内容完整展示出来,基本可以无视。

js 复制代码
import {GrandChild} from "./GrandChild";
export const Child = () => {
  return (
    <div>
      <GrandChild />
    </div>
  );
}

GrandChild.ts

js 复制代码
import { useContext } from "react";
// 引入主题上下文
import { ThemeContext } from "./ThemeContext";

export const GrandChild = () => {
  // 使用 useContext 获取当前主题
  const { theme } = useContext(ThemeContext);
  return (
    <div>
      <h3>孙组件 ------ {theme}</h3>
    </div>
  );
}

此时,父组件的数据可以不用经过层层传递才来到祖孙组件。

复制代码
// useContext 就像:
1. 我有个快递柜(createContext)
2. 我把包裹放进去(Provider value={包裹})
3. 你在任何地方都能取出来用(useContext)

// 以前传递数据(麻烦):
爸爸 → 儿子 → 孙子 → 重孙

// 现在用 useContext(简单):
爸爸 → 快递柜 → 重孙(直接取)

在实际项目中我们通常把 useContext 封装起来。

useTheme.ts

js 复制代码
import { useContext } from "react";
import { ThemeContext } from "./ThemeContext";

export const useTheme = () => {
  // 使用 useContext 获取当前主题
  const { theme } = useContext(ThemeContext);
  return theme;
}

GrandChild.ts 改为

js 复制代码
import { useTheme } from "./useTheme";

export const GrandChild = () => {
  // 使用自定义的 useTheme Hook 获取当前主题
  const theme = useTheme();
  return (
    <div>
      <h3>孙组件 ------ {theme}</h3>
    </div>
  );
}

注:

  1. 必须在 Provider 内部才能拿到值,否则拿默认值
  2. Provider 的 value 变化,所有用 useContext 的组件都更新
  3. 可以创建多个​ Context,分别放不同数据。按功能拆分,不要把所有状态放在一个 Context

什么时候应该使用 useContext?

  • 当需要跨多层级组件共享状态时
  • 全局状态如:用户信息、主题、语言、通知等
  • 避免 props drilling 时

useContext 和 Redux 的区别?

  • useContext + useReducer:适合中小型应用,内置于 React
  • Redux:适合大型复杂应用,有强大的中间件和开发者工具

Context 更新会导致所有子组件重新渲染吗?

  • 是的,但可以通过以下方式优化:
  1. 拆分 Context
  2. 使用 React.memo
  3. 使用 useMemo记忆 context 值

useMemo

useMemo是 React 中的一个 Hook,用于缓存计算结果,避免在每次渲染时都进行昂贵的计算。

基本语法: const result = useMemo(() => compute(), deps)

js 复制代码
const result = useMemo(
  // 第一个参数:计算函数
  () => {
    // 1. 这里可以包含任意逻辑
    // 2. 必须是同步函数
    // 3. 应该是纯函数(无副作用)
    // 4. 返回任意类型的值
    return someValue;
  },
  // 第二个参数:依赖数组
  []
);

返回值: result:接收 计算函数 的返回值,缓存计算的结果。

参数:

  • 第一个参数:() => compute(),计算函数 (factory function),返回任意类型的值。记得 return
  • 第二个参数:依赖数组,决定何时重新计算

依赖数组的三种状态:

  1. 空数组 []- 只计算一次
js 复制代码
const stableValue = useMemo(() => {
  console.log('这个只会在首次渲染时计算一次');
  return expensiveInitialization();
}, []); // 空数组
  1. 有依赖项 - 依赖变化时重新计算
js 复制代码
const dynamicValue = useMemo(() => {
  console.log('依赖变化时重新计算');
  return calculate(a, b, c);
}, [a, b, c]); // 指定依赖
  1. 不传依赖 - 每次渲染都重新计算(不推荐)
js 复制代码
const alwaysRecalc = useMemo(() => {
  console.log('每次渲染都计算,相当于没用 useMemo!');
  return calculate();
}); // 不传依赖数组

依赖比较的详细规则:浅比较

  1. 基本类型的比较:比较值是否相等
js 复制代码
function BasicTypes({ count, name }) {
  const result = useMemo(() => {
    return `Count: ${count}, Name: ${name}`;
  }, [count, name]); // 基本类型:比较值是否相等
  
  return <div>{result}</div>;
}
  1. 引用类型的比较:比较引用地址
js 复制代码
function ReferenceTypes({ config }) {
  const result = useMemo(() => {
    return process(config);
  }, [config]); // 对象:比较引用地址
  
  // 问题:即使 config 内容相同,引用不同也会重新计算
  return <div>{result}</div>;
}

一般对象依赖我们都是用对象属性,而不是整个对象,避免引起不要的渲染

js 复制代码
function ObjectDeps({ config }) {
  const result = useMemo(() => {
    return config.value1 + config.value2;
  }, [config.value1, config.value2]); // 使用具体属性,而不是整个对象
  
  return <div>{result}</div>;
}

useMemo缓存的是计算的结果,而不是计算的过程。它通过比较依赖数组来决定是否需要重新计算,并返回稳定引用的值。

React.memo

当父组件重新渲染时,想要防止不必要的子组件重新渲染。我们可以用 React.memo 来优化性能,特别是对于渲染成本较高的组件。

基本语法

js 复制代码
const MyComponent= (props) => <div>{props.text}</div>;
const MemoizedComponent = React.memo(MyComponent, areEqual?);

返回值 :返回一个全新的组件,这个新组件包装了原始组件,并添加了记忆(缓存)功能。不是修改原始组件
参数

  • 第一个参数:要包装的组件,必须是函数组件(包括箭头函数组件)。
  • 第二个参数:可选的比较函数
js 复制代码
type CompareFunction = (
  prevProps: Readonly<P>,  // 上一次的 props
  nextProps: Readonly<P>   // 下一次的 props
) => boolean;              // true: 跳过渲染,false: 重新渲染
js 复制代码
const ActionButton = React.memo(
  ({ label, onClick, icon }) => {
    return (
      <button onClick={onClick}>
        {icon && <span>{icon}</span>}
        {label}
      </button>
    );
  },
  (prevProps, nextProps) => {
    // 比较 label 和 icon
    return (
      prevProps.label === nextProps.label &&
      prevProps.icon === nextProps.icon
    );
  }
);

如果不提供第二个参数,React 使用默认的浅比较

js 复制代码
import { memo, useState } from "react";

const MomoSon = memo(() => {
  console.log('子组件渲染了');
  return (
    <div>
      <h3>子组件</h3>
    </div>
  );
})

export const Parent = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h2>父组件</h2>
      <button onClick={() => setCount(count + 1)}>
        {count}
      </button>
      {/* 子组件不会打印console */}
      <MomoSon />
    </div>
  );
}

useCallback

useCallback是 React 中的一个 Hook,它的作用是缓存函数引用,避免不必要的重新创建函数,从而优化性能。

主要作用

  1. 避免子组件不必要的重新渲染 - 当父组件重新渲染时,传递给子组件的函数引用会改变,导致子组件即使使用了 React.memo也会重新渲染
  2. 依赖数组优化 - 只有在依赖项发生变化时,才会重新创建函数
  3. 配合其他 Hook 使用 - 常与 useMemo、memo配合使用

基本语法

js 复制代码
import { useCallback } from 'react';

const memoizedCallback = useCallback(
  () => {
    // 函数逻辑
    doSomething(a, b);
  },
  [a, b], // 依赖数组
);

依赖数组的两种状态:

  1. 空数组,只在组件挂载时创建一次
js 复制代码
import React, { useState, useCallback, memo } from 'react';

// 子组件
const Child = memo(({ onClick }) => {
  console.log('Child 重新渲染了');
  return <button onClick={onClick}>点击</button>;
});

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);
  
  // 不使用 useCallback - 每次父组件渲染都会创建新函数
  // const handleClick = () => console.log('clicked');
  
  // 使用 useCallback - 函数引用保持不变
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []); // 空依赖数组,只在组件挂载时创建一次
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
      <Child onClick={handleClick} />
    </div>
  );
};
  1. 数组有值,当值发生改变时,重新创建函数
js 复制代码
const MyComponent = ({ userId, data }) => {
  const [count, setCount] = useState(0);
  
  // 当 userId 或 data 变化时才重新创建函数
  const fetchData = useCallback(async () => {
    const result = await fetch(`/api/user/${userId}`, {
      body: JSON.stringify(data)
    });
    return result;
  }, [userId, data]); // 依赖项
  
  useEffect(() => {
    fetchData();
  }, [fetchData]); // fetchData 引用稳定
  
  return <div>Count: {count}</div>;
};

与 useMemo 的区别

  • useCallback 缓存函数
  • useMemo 缓存值

结合 React.memo 使用

js 复制代码
const ExpensiveComponent = memo(({ onClick, data }) => {
  // 昂贵计算
  return <div onClick={onClick}>{data}</div>;
});

const Parent = () => {
  const [state, setState] = useState();
  const handleClick = useCallback(() => {
    // 处理点击
  }, []);

  return <ExpensiveComponent onClick={handleClick} data={state} />;
};

记住:**不要过早优化。**大多数情况下,函数创建的开销很小,只有在性能确实成为问题时再考虑使用 useCallback。

相关推荐
阿凤212 小时前
后端返回数据流的格式
开发语言·前端·javascript·uniapp
懂懂tty2 小时前
React Hooks原理
前端·react.js
00后程序员张2 小时前
前端可视化大屏制作全指南:需求分析、技术选型与性能优化
前端·ios·性能优化·小程序·uni-app·iphone·需求分析
kyriewen2 小时前
屎山代码拆不动?微前端来救场:一个应用变“乐高城堡”
前端·javascript·前端框架
@大迁世界2 小时前
3月 React 圈又变天了
前端·javascript·react.js·前端框架·ecmascript
忆江南2 小时前
# iOS 稳定性方向常见面试题与详解
前端
陆枫Larry2 小时前
一次讲清楚 `Promise.finally()`:为什么“无论成功失败都要执行”该用它
前端
Momo__2 小时前
被低估的 HTML 原生表单元素:dialog、datalist、meter、progress
前端
莹宝思密达2 小时前
【AI】chrome-dev-tools-mcp
前端·ai