0%

React

学习视频

12. Props, Immutability, and One-Way Data Flow_哔哩哔哩_bilibili

React笔记


前置知识

这里写了常用到的 js知识

//对象解构赋值(Object Destructuring),允许你从对象中提取属性,并将它们赋值给变量
const { title,author,publicationDate,hasMovieAdaptation,genres,reviews,translations } = book;
console.log(title, author);

//数组解构赋值允许你将数组中的值提取到单独的变量中,语法类似于对象解构赋值,但使用的是方括号 [];
const [firstBook, secondBook, ...otherBooks ] = books;
console.log(firstBook, secondBook, otherBooks);
//添加新元素 在解构赋值中,... 操作符可以用来收集剩余的元素。它可以用于数组解构时将剩余的元素赋值给一个新数组
const newGenres = [...book.genres, "horror"];
console.log(newGenres);

//adding new genre to books可以覆盖原数组,也可以添加新的元素
//先展开
const updateBook = { ...books, data:"123" };
updateBook;

//模板字符串
const summary = `${title} is a book,and published in ${publicationDate.split("-")[0]}`;
summary;

//三元运算符
const hasMovie = books.hasMovieAdaptation ? "Yes" : "No";
hasMovie;

//箭头函数 split() 方法用于分割字符串,并返回一个包含分割后的子字符串的数组。
const publicationDateYear = (str) => str.publicationDate.split("-")[0];
console.log(publicationDateYear(book));
//运算符
// && 短路逻辑与 || 短路逻辑
console.log( true && "nihao");
console.log( false || "nihao");
// console.log( true || "nihao");
// console.log( false && "nihao");

//?? false短路逻辑 总评
const count = book.reviews.librarything ?? 0;
count;

//falsy values: false, null, undefined, 0, NaN, ""
//truthy values: true, non-empty string, non-empty array, object, function


//optonal chaining
function getTotalReviewcount(book){
const goodreads = book.reviews?.goodreads;
const librarything = book.reviews?.librarything ?? 0;
return goodreads + librarything;
}
const totalReviewCount = getTotalReviewcount(book);
totalReviewCount;

//map会遍历数组中的每一个元素,并按照函数式,对原数组的每一项进行操作,返回一个新的数组
const x =[1,2,3,4,5].map(num => num * 2);
console.log(x);

// reduce 的效果 是将数组中的元素合并成一个值 相较于 filter、map
const arr = [1,2,3,4,5];
const sum = arr.reduce((sum, cur) => sum + cur, 0);
console.log(sum);

//filer 过滤数组中的元素,返回一个新的数组
const arr3 = [1, 3, 2, 4, 5];
const filteredArr = arr3.filter((num) => num % 2 === 0);
filteredArr;

//sort 会改变原数组,返回排序后的数组
const arr1 = [1, 3, 2, 4, 5];
const sortedArr = arr1.sort((a, b) => a - b);
sortedArr;
arr1;

// slice 可以返回一个新数组,不会改变原数组
const arr2 = [1, 3, 2, 4, 5];
const slicedArr2 = arr2.slice(0, 5).sort((a, b) => a - b);
console.log(slicedArr2);
arr2;

//箭头函数 为了延迟执行 onDeleteItem(item.id),即在点击时才调用它
onClick={() => onDeleteItem(item.id)}

//回调函数 作为参数传递给另一个函数
function greet() {
console.log("Hello!");
}

setTimeout(greet, 2000); // 2秒后调用greet函数


概述

React 是用于构建用户界面的 JavaScript 库。

它提供了组件、状态管理、生命周期方法等功能来帮助开发者构建复杂的前端应用

其主要功能是实现后端数据与前端页面的即时更新,同时减少单一使用js时的繁冗工作量

项目结构

创建项目:

方法1:

a. 安装 Node.js 和 npm(Node.js 包管理器)。

b. 打开终端或命令提示符。

c. 运行以下命令来创建一个新的 React 应用程序:

npx create-react-app my-app
cd my-app
npm start
方法2:使用 Vite

Vite 是一个轻量级的构建工具,它提供了快速的冷启动和热更新。它支持 TypeScript、CSS 预处理器和许多其他功能。

npm init vite@latest my-app -- --template react
cd my-app
npm install
npm run dev

文件结构如下

my-app/
├── node_modules/ # 项目依赖的第三方库
├── public/
│ ├── favicon.ico # 网站图标
│ ├── index.html # HTML 入口文件
│ ├── manifest.json # PWA 应用配置文件
│ └── robots.txt # 爬虫协议文件
├── src/
│ ├── App.css # 应用的样式文件
│ ├── App.tsx # 应用的主组件文件 (或 .js)
│ ├── App.test.tsx # 测试文件,使用 Jest 测试框架
│ ├── index.css # 全局样式文件
│ ├── index.tsx # 应用入口文件 (或 .js)
│ ├── logo.svg # 应用 logo 图标
│ ├── reportWebVitals.ts # 性能报告 (可选)
│ └── setupTests.ts # 设置测试环境的配置文件
├── .gitignore # Git 忽略文件配置
├── package.json # 项目配置文件,包含依赖信息和脚本
├── README.md # 项目的 README 文件
├── tsconfig.json # TypeScript 配置文件(如果使用 TypeScript)
└── yarn.lock / package-lock.json # 锁定依赖版本的文件

React 三要素

1. 组件(Component)

组件是 React 的基础构建块,每个 React 应用都是由一个个组件组合而成的。React为每一个组件渲染一个视图,这些视图组成UI。每个组件都拥有自己的 “数据” “(js)逻辑” “外观

  • react笔记/react类组件:通过 ES6 的类来定义,包含状态(state)和生命周期方法。
  • 函数组件:React 16.8 之后推荐的方式,使用函数定义组件并结合钩子(Hooks)来管理状态和生命周期。

组件不可嵌套

示例

// 函数组件
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

什么是 JSX ?

jsx是一种声明语法,用来描述组件的外观,根据数据和逻辑工作。通常与 React 一起使用,允许在 JavaScript 代码中编写类似 HTML 的标签

  1. 类 HTML 的语法
  • JSX 允许在 JavaScript 代码中嵌入类似 HTML 的标签,使得编写用户界面时更加直观和简洁。虽然看起来像 HTML,但 JSX 实际上会被编译成 JavaScript 函数调用。
  1. 支持表达式
  • JSX 中可以使用 JavaScript 表达式,通过{}包裹

    const name = 'Alice';
    const element = <h1>Hello, {name}!</h1>;
  1. 必须返回一个父元素
  • 在 JSX 中,所有标签必须被一个父级元素包裹。例如:

    return (
    <div>
    <h1>Title</h1>
    <p>Description</p>
    </div>
    );
  1. 样式和属性的处理
  • 在 JSX 中,class属性被替换为className,style属性接受一个对象。例如:

    <div className="container" style={{ color: 'red' }}>Hello</div>

2. 状态(State)

状态是组件内部的数据,它决定了组件的行为和显示的内容。

状态是可变的,随着用户的交互或其他事件触发,它会发生变化并重新渲染组件。

在类组件中,状态通过 this.state 来管理,在函数组件中使用 useState 钩子来管理。

什么是钩子?

在 React 中,钩子就是use开头的状态组件,允许你在函数组件中使用状态和其他 React 特性,而不需要编写类组件。

常见的 React 钩子有:

  • useState:用于在函数组件中添加状态变量。
  • useEffect:用于执行副作用操作(如数据获取、订阅或手动更改 DOM)。
  • useContext:用于在组件树中共享数据,而无需逐层传递 props。

怎么使用状态

什么时候创建状态?

  1. 需要存储数据?

  2. 数据会发生变化吗

    • 不会 => const 常量即可
  3. 是否可以从现有的道具/状态中计算?

    • 会 => 派生状态(derive state)
  4. 更新状态是否需要重新渲染组件?

  • 不会 => 使用Ref (像普通状态一样持久的保持数据,但无需重新渲染组件)
  1. 使用useState创建一个状态,并放置在组件中

在哪里使用状态?

当前组件、通过道具传递给子组件、将状态传递给同级组件的公共父组件中去(状态上移)、所有组件使用(全局状态

派生状态

派生状态就是简单地从一个现有状态或者道具中计算出来的状态

const [total,setTotal] = useState(0);
const tip = total /2;

3. 属性(Props)

props 是组件之间传递数据的方式。

父组件可以通过 props 向子组件传递数据,子组件无法修改 props,它们是只读的。props 的主要作用是让组件的渲染更加灵活和可复用

在传递中,我们可以使用 js 中的重构来传递对象的名称而不是props,但是不要忘记使用{}

注 : React是单向数据流,数据只能从父组件流向子组件

示例

// 父组件向子组件传递数据
function Menu() {
return (
<main className="menu">
<h2>our pizzas</h2>
<Pizza name= "Focaccia"
ingredients= "Bread with italian olive oil and rosemary"
price="6"
photoName="pizzas/focaccia.jpg"/>
</main>

function Pizza(props) {
return (
<div className="pizza">
<img src={props.photoName} alt={props.name} />
<div className="pizzas">
<h3>{props.name}</h3>
<p>{props.ingredients}</p>
<span>{props.price}</span>
</div>
</div>
);
}

子代道具

在 React 中,子代道具(Children Props) 是指通过 props.children 传递的内容,它允许父组件将嵌套在其内部的 JSX 代码或组件传递给子组件。它提供了一种灵活的方式来构建可复用的组件,使得父组件可以决定子组件的内部内容,而不需要在子组件中明确指定。

示例:

//子组件
function Wrapper(props) {
return <div className="wrapper">{props.children}</div>;
}

//父组件
function App() {
return (
<Wrapper>
<h1>Hello, World!</h1>
<p>This is a paragraph inside the Wrapper component.</p>
</Wrapper>
);
}

三要素关系总结:

  • 组件 是 UI 的基本单元,通过组合形成完整的应用。
  • 状态 是组件内部的动态数据,控制着组件的行为和显示。
  • 属性(props) 用于在组件间传递数据,通常是父组件传递给子组件的数据。

状态与道具有什么区别?

状态(state):状态是组件内部管理的数据,数据归创建他的组件所有,它可以看作组件的存储,可以用来长期保存数据。状态可以在组件内部被修改(通过setStateuseState),并会在变化时触发组件重新渲染,用来控制组件的行为、渲染和交互。

  • 用于存储组件内部动态变化的数据,比如用户输入的表单值、加载数据的结果、组件的交互状态(如打开或关闭某个UI元素)。
  • 当状态发生变化时,React会自动触发两个组件的重新渲染

道具(props):道具是从父组件传递给子组件的数据,数据被父组件所有,可以把它想象函数参数。因为它是只读的,子组件无法直接修改它。当子组件收到更新的props时,也会重新渲染组件

  • 道具主要用来让组件间进行静态数据传递,通常是父组件将数据通过道具传递给子组件,子组件根据道具的值渲染。
  • 当道具的值在父组件中发生变化时,子组件会重新渲染。

怎么渲染列表(rendering a list)

概念:可以理解为创建一个数组,并为数组里的每个元素创建组件

通常我们会使用数组的 map() 方法来生成列表项。每个列表项需要一个唯一的 key 属性,以帮助 React 识别和优化列表中的元素

示例:

//const pizzaData = [{...}]
function Menu() {
return (
<main className="menu">
<h2>our pizzas</h2>
<div>
{pizzaData.map((pizzas) => (
<Pizza pizzaObj={pizzas} key={pizzas.name} /> ))}
{/*这一段代码是 React 中通过数组的 map() 方法渲染列表的常用方式。它的作用是遍历 pizzaData 数组,为数组中的每个对象生成一个对应的 Pizza 组件*/}
</div>
</main>
);
}
function Pizza(props) {
return (
<div className="pizza">
<img src={props.pizzaObj.photoName} alt={props.pizzaObj.name} />
<div className="pizzas">
<h3>{props.pizzaObj.name}</h3>
<p>{props.pizzaObj.ingredients}</p>
<span>{props.pizzaObj.price}</span>
</div>
</div>
);
}

条件渲染

方法一:使用 && 运算符的短路逻辑,在特定条件下选择内容

示例:

function Footer() {
const data = new Date().getHours();
console.log(data);
const open = 20;
const close = 22;
const isOpen = data >= open && data < close;
return (
<footer className="footer">
{isOpen && <p> Open</p> }
</footer>
);
}

方法2:使用三元运算符

示例:

function Footer() {
const data = new Date().getHours();
console.log(data);
const open = 20;
const close = 22;
const isOpen = data >= open && data < close;
return (
<footer className="footer">
{isOpen = true ?(<p>Open</p>): null}
</footer>
);
}

方法三:多重返回

示例:

function Footer() {
const data = new Date().getHours();
console.log(data);
const open = 20;
const close = 22;
const isOpen = data >= open && data < close;

if (!isOpen) {
return (<p>Close</p>);
}

return (
<footer className="footer">
{/* {isOpen && */}
<div className="order">
<p> We're open for orders from {open} to {close}. Call us at 123-456-7890.</p>
<button className="btn" style={{color: 'yellow'}} >Order</button>
</div>
</footer>
);
}

React Fragment(react 片段)

概念:React.Fragment 是 React 中用于包裹多个子元素而不额外生成 HTML 元素的组件。

通常在 JSX 中,如果你返回多个元素,它们需要被一个父级元素<div></div>包裹,而 React.Fragment 可以在不生成额外 DOM 节点的情况下包裹这些元素。

示例:

function App() {
return (
<react.fragment>
<Header/>
<Menu />
<Footer/>
</react.fragment>
);
}
//或者
function App() {
return (
<>
<Header/>
<Menu />
<Footer/>
</>
);
}

如何在React中使用表单?

怎么判断是否需要使用表单?

  1. 用户输入:如果你需要用户输入数据,比如添加待办事项,那么你需要一个表单。表单可以包含输入框、文本域、选择框等元素,让用户输入信息。
  2. 提交操作:如果你需要用户提交数据,比如添加待办事项后保存到列表中,那么你需要一个表单。表单通常包含一个提交按钮,用户点击后触发数据提交。
  3. 数据验证:如果你需要对用户输入的数据进行验证,比如检查待办事项是否为空,那么你需要一个表单。表单可以包含验证逻辑,确保用户输入的数据符合要求。
  4. 状态管理:如果你需要管理用户输入的状态,比如输入框的值,那么你需要一个表单。表单可以包含状态管理逻辑,确保用户输入的数据能够被正确处理。
  5. 事件处理:如果你需要处理用户输入的事件,比如输入框的 onChange 事件,那么你需要一个表单。表单可以包含事件处理逻辑,响应用户的操作。

表单完整代码(以ToDoList为例):

function Form (){

const [description, setDescription] = useState('');
const [quantity, setQuantity] = useState(1);

function handleSubmit(e){
e.preventDefault();
}

return(
<form className="add-form" onSubmit={handleSubmit}>
<h3>What do you need for your trip</h3>
<select value={quantity} onChange={(e) => setQuantity(Number(e.target.value)} >
{Array.from({length: 20}, (_, i) => i+1).map
((num) => (
<option value={num} key={num}>
{num}
</option>))}
</select>
<input type="text"
placeholder="Item..."
value={description}
onChange={(e) => setDescription(e.target.value)}/>
<button>Add</button>
</form>
);
}

渲染列表

{Array.from({length: 20}, (_, i) => i+1).map ((num) => ( <option value={num} key={num}> {num} </option>))}:

利用Array.from()生成一个数组,并通过map()来遍历数组,动态生成一组<option>标签

onChange:当用户在输入框中输入内容时,onChange事件会触发,更新React状态,从而动态更新表单值

如何将表单中的数据导入事件?

常见的React事件类型:

  • **onClick**:点击事件
  • **onChange**:表单元素(如输入框、选择框)的值改变事件
  • **onSubmit**:表单提交事件
  • **onKeyDownonKeyUp**:键盘按下、松开事件
  • **onMouseEnteronMouseLeave**:鼠标进入、离开事件

受控元素

在React中,使用受控组件可以将HTML表单中的数据与组件的状态(state)直接绑定,从而避免直接操作DOM。这样做的好处是表单数据的更新和管理完全交由React来处理,无需手动获取DOM元素的值

三个步骤:

  • 设置状态 const [description, setDescription] = useState('');

    • 目的:创建一个变量来存储用户输入的值,并在用户与表单交互时,React能够追踪和管理这个值。
  • 绑定变量 通过value属性,将表单元素(例如<input><textarea>)的值与刚刚创建的状态变量绑定。这样,表单的值由状态控制,React状态和UI之间建立了双向绑定关系。

    • 操作:在表单元素上使用value={description},使输入框的值与description状态保持同步
  • 更新状态 onChange={(e) => setDescription(e.target.value)},当用户在表单元素中输入内容时,onChange事件会触发。此时,你可以使用setDescription函数更新状态,确保状态随用户输入的变化而改变。事件处理函数将接收事件对象,通过e.target.value获取表单元素的新值。

    • 操作:在onChange事件处理函数中调用setDescription(e.target.value),将表单的新值更新到状态中

    • js小知识

      onChange={(e) => setDescription(e.target.value)} 这段代码中,e 是一个事件对象(Event Object),它代表了当前正在发生的事件。在 JavaScript 中,当事件(如点击、输入等)发生时,浏览器会创建一个事件对象,该对象包含了与该事件相关的所有信息。

      在这个特定的例子中,e 是一个合成事件(Synthetic Event)对象,它是 React 为了兼容不同浏览器而创建的一个抽象层。合成事件对象封装了原生浏览器事件,并提供了一个统一的接口,使得开发者可以编写跨浏览器的代码。

      e.target 是事件的目标元素(Event Target),即触发事件的 DOM 元素。在这个例子中,e.target 是输入框(input)元素。

      e.target.value 是输入框元素的当前值(Value),即用户输入的内容。


Router

React Router 是一个用于 React 应用的库,用于管理应用的路由和导航。它的主要作用是帮助开发者构建单页面应用(SPA),通过动态加载组件而不是每次跳转都重新加载整个页面。

React Router 的作用和意义

  1. 单页面应用(SPA)支持

    • React Router 允许应用在不刷新页面的情况下切换视图,提升用户体验。
    • 通过动态加载组件,减少页面加载时间,提升性能。
  2. URL 与 UI 同步

    • React Router 保持 URL 与 UI 同步,用户可以通过 URL 直接访问特定页面,便于分享和书签。
  3. 嵌套路由

    • 支持嵌套路由,允许在父组件中嵌套子组件,便于构建复杂的 UI 结构。
  4. 动态路由

    • 支持动态路由,允许根据 URL 参数动态加载不同内容。
  5. 编程式导航

    • 提供编程式导航,开发者可以通过代码控制页面跳转,而不是依赖用户点击链接。
  6. 路由守卫

    • 支持路由守卫,可以在用户访问特定路由前进行权限检查或数据加载。

为什么不直接用链接

  1. 页面刷新

    • 使用普通链接会导致页面刷新,破坏 SPA 的无刷新体验。
  2. 状态丢失

    • 页面刷新会导致应用状态丢失,而 React Router 可以在不刷新页面的情况下保持状态。
  3. 复杂路由管理

    • 普通链接无法处理复杂的路由逻辑,如嵌套路由、动态路由等。
  4. 编程式导航

    • 普通链接无法实现编程式导航,React Router 提供了更多控制权。
import React from 'react';
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';

function Home() {
return <h1>Home</h1>;
}

function About() {
return <h1>About</h1>;
}

function App() {
return (
<div>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" component={About} />
</Switch>
</div>
);
}

function Root() {
return (
<Router>
<App />
</Router>
);
}

export default Root;

useEffect

什么是useEffact?

useEffect 是 React 的一个 Hook,用于在函数组件中处理副作用

useEffect 让函数组件能在渲染后执行副作用,并通过依赖项和清理函数精准控制执行时机,替代类组件的生命周期方法(如 componentDidMount / componentDidUpdate / componentWillUnmount)。它的核心作用是:

1. 什么是副作用?
  • 副作用是指那些可能影响其他组件或与外部世界交互的操作,例如:
    • 数据获取(API 请求)
    • 手动修改 DOM
    • 订阅事件(如 window.addEventListener
    • 定时器(setTimeout / setInterval
用法
useEffect(() => {
// 副作用代码(组件渲染后执行)
return () => {
// 清理函数(组件卸载或依赖项变化前执行)
};
}, [依赖项数组]); // 控制副作用触发的条件

依赖项数组这一列中,不用纠结太多,一般把所有的外部值都添加进来就可以。

  • 无依赖数组([]:只在组件首次渲染后执行一次(类似 componentDidMount)。
  • 有依赖项([a, b]:当 ab 变化时重新执行。
  • 无第二个参数每次组件更新后都执行(谨慎使用!)。
  • 清理函数:用于取消订阅、清除定时器等(类似 componentWillUnmount)。

为什么需要useEffect?

以获取数据为例:

const { useState } = require("react");

function backend(){
const [isLoading,setIsLoading] = useState(true);
const [list,setList] = useState([]);
fetch(
'https://react-getting-started-48dec-default-rtdb,firebaseio.com/meetups.json'
).then((response)=>{
return response.json()
}).then((data)=>{
setIsLoading(false);
setLoadedMeetups(data);})
if (isLoading){
return(
<div>Loading...</div>
)
}

return(
<div>
<List date={list} />
</div>
)
}

export default backend;

这样的写法会陷入死循环,原因是 fetch 请求被放在了函数组件的顶层作用域中,而每次组件渲染时都会重新执行 fetch 请求,导致状态更新(setIsLoadingsetList),进而触发组件重新渲染,然后再次执行 fetch 请求,形成无限循环。

采用useEffct:

const { useState } = require("react");

function backend(){
const [isLoading,setIsLoading] = useState(true);
const [list,setList] = useState([]);

useEffect()=>{
fetch(
'https://react-getting-started-48dec-default-rtdb,firebaseio.com/meetups.json'
).then((response)=>{
return response.json()
}).then((data)=>{
setIsLoading(false);
setLoadedMeetups(data);
});
},[isLoading])

return(
<div>
<List date={list} />
</div>
)
}

export default backend;

Context - 上下文

上下文(Context) 是 React 提供的一种 跨组件层级传递数据 的机制,用于避免繁琐的逐层 props 传递(俗称“props 钻取”)。它特别适合全局共享的数据(如主题、用户登录状态、多语言配置等)。

  1. 核心 API
    • React.createContext():创建一个 Context 对象(包含 ProviderConsumer)。
    • Context.Provider:提供数据的组件,通过 value 属性传递数据。
    • useContext():函数组件中消费 Context 数据的 Hook(替代 Consumer

使用示例:

import { createContext, useContext,useState } from "react";

// 1. 创建初始 Context(建议命名以 Context/Provider 结尾)
const TestContext = createContext({
favorites: userFavorites,
number: userFavorites.length,
});

// 2. 提供/更新数据的 Provider 组件
function TestProvider({ children }) {
//const sharedData = "这是要共享的数据"; // 通常用 useState 管理动态数据
const [userFavorites,setUserFavorites] = useState([]);
const context = {
favorites: userFavorites,
number: userFavorites.length,
};

return (
<TestContext.Provider value={context}>
{children}
</TestContext.Provider>
);
}

// 3. 自定义 Hook 简化(可选)
export function useTest() {
return useContext(TestContext);
}

// 使用示例
function App() {
return (
<TestProvider>
<ChildComponent />
</TestProvider>
);
}

function ChildComponent() {
const data = useTest(); // 或直接 useContext(TestContext)
return <div>{data}</div>; // 输出: "这是要共享的数据"
}

当多个页面都需要使用当前上下文时,可以用一个provider文件专门封装:

'use client'

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { SuiClientProvider, WalletProvider } from "@mysten/dapp-kit";
import { networkConfig, network } from "@/contracts"
import "@mysten/dapp-kit/dist/index.css";
import { ThemeProvider } from "next-themes";
import { UserProfileProvider } from "@/contexts/user-profile-context";
import { AppProvider } from "@/contexts/app-context";

const queryClient = new QueryClient();

export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<QueryClientProvider client={queryClient}>
<SuiClientProvider networks={networkConfig} defaultNetwork={network}>
<AppProvider>
<UserProfileProvider>
<WalletProvider autoConnect stashedWallet={
{
name: "Sui Passport",
network: network,
}
}>
{children}
</WalletProvider>
</UserProfileProvider>
</AppProvider>
</SuiClientProvider>
</QueryClientProvider>
</ThemeProvider>
);
}

🤔 那什么时候不用全局包裹?

如果 profile 只在某些特定路由中使用,比如 /profile/*,那你可以只在某个 layout 里包裹:

tsxCopyEdit// app/profile/layout.tsx
import { UserProfileProvider } from '@/contexts/UserProfileProvider';

export default function ProfileLayout({ children }) {
return (
<UserProfileProvider>
{children}
</UserProfileProvider>
);
}