从Props透传到自定义Hook:系统梳理React跨层级通信与逻辑复用

从Props透传到自定义Hook:系统梳理React跨层级通信与逻辑复用

很多同学初学React时,都会从父子组件props传参开始入门------父组件往下传数据,子组件通过自定义事件往回传,这是最直觉、最好理解的通信方式,也是每个React学习者必经的第一课。但随着业务迭代、组件嵌套越来越深,你会慢慢发现:层层传递数据的写法开始变得臃肿,中间层组件被迫接收并转发大量自己根本用不到的数据,改一处要动一串,维护成本直线上升。

这时候,你就需要往上走一个台阶了。本文从组件通信的核心痛点出发,带你系统吃透 React Context + useContext 的跨层级通信方案,再通过自定义Hook实现逻辑复用与工程化分层,最后结合可落地的实战案例,帮你建立从基础API到架构思维的完整知识体系------无论你是刚学完props的新手,还是已经能写组件、但一遇到深层嵌套就头疼的进阶者,都能沿着这条路线找到自己的下一个台阶。


一、先搞懂:React组件通信的四大场景与痛点

在React的组件化开发模式中,不同组件之间的数据传递是最基础也最核心的需求。按照组件关系的远近,我们可以把它们排成一张「由易到难」的地图:

通信场景 典型方案 学习难度
父子组件通信 父组件通过props向下传数据,子组件通过自定义事件向上回调 ⭐ 入门第一课
兄弟组件通信 把状态提升到共同的父组件,由父组件统一管理分发 ⭐⭐ 稍加思考
祖先后代组件通信 数据需要经过多层中间组件层层接力传递 ⭐⭐⭐ 开始难受
陌生人组件通信 组件树中几乎没有交集,无公共近层级父组件 ⭐⭐⭐⭐ 进阶必备

把这张表再往深看一层,每个场景其实都有明确的方案边界,而不是笼统地一句「用 Context 就行」:

  • 父子、兄弟:props + 状态提升就够了,不需要上 Context。当兄弟组件的公共祖先层级很深时,单纯「状态提升」会让父组件变得臃肿,这时候自然演进出**「状态提升到公共祖先 + Context 跨层级消费」**的组合方案------这也是 props 和 Context 在真实项目里最常见的混用姿势。
  • 祖先后代 :如果公共祖先层级不深,优先用组件组合(见下文补充);层级深、组合不好使时,才上 Context。
  • 陌生人组件 :如果两个组件存在公共祖先 ,把 Provider 放在公共祖先层级,Context 依然能覆盖;如果是完全跨组件树的场景(比如 Portal 渲染的全局弹窗、独立悬浮面板),原生 Context 够不到,这时才需要 Zustand、Redux 这类全局状态管理库。

前两种场景,用props都能优雅解决------这也是你入门时最先掌握的能力。问题出在第三行:组件层次比较深的时候。

假设有这样一个结构:

复制代码
App → Parent → Child → GrandChild

如果 GrandChild 需要 App 里的一个 theme 数据,那么 ParentChild 这两个「中间商」明明用不到这个数据,却被迫接收并继续往下传。

这就是臭名昭著的 Prop Drilling(属性穿透):数据像接力棒一样一层层往下传,中间的组件哪怕根本用不到这份数据,也必须接收并继续向下传递。层级浅的时候还能接受,一旦组件树达到三四层以上,数据搬运的工作量就会非常大,后续修改数据结构还要改动所有中间层。

我的学习笔记里,就保留了这段「反面教材」:

jsx 复制代码
// function App() {
//   return (
//    <>
//     <parent>
//      <child>
//       <grandchild>
//       </grandchild>
//      </child>
//     </parent>
//    </>
//   )
// }
// 这段代码是在演示 Prop Drilling:数据像接力棒一样一层层传,
// 中间的组件被迫接收和传递不需要的数据,也就是为什么需要 Context

核心矛盾:组件之间需要共享数据,但数据传递的路径和组件的层级结构被强行绑定了。


补充:掏 Context 之前,先试试 React 官方的「第一道防线」------组件组合

很多人一遇到 Prop Drilling,第一反应就是「上 Context」。但 React 官方其实更希望你先看看能不能用组件组合(Component Composition)绕过中间层

什么意思?看代码:

jsx 复制代码
// 中间的 Parent、Child 只负责渲染 children,完全不用接收和转发 theme
function App() {
  const [theme, setTheme] = useState('light')
  return (
    <Parent>
      <Child>
        <GrandChild theme={theme} />
      </Child>
    </Parent>
  )
}

theme 数据从 App 直接以 JSX 的形式传给了 GrandChildParentChild 只负责渲染 children完全不需要感知 theme 的存在。数据传递路径没变,但中间层的「搬运负担」被彻底消除了。

这就是 React 官方推荐的心智模型:

能组合解决,就别上 Context;能 Context 解决,就别上状态管理库。

组合解决不了的时候(比如中间层需要根据数据做条件渲染、或者数据要被多个不同分支消费),再掏 Context 出来。这不是 Context 不好用,而是工具箱里每把工具都有它最合适的位置

useContext 正是为了解决组合解决不了的那部分问题而生的------它提供了一套跨层级直接共享数据的机制,让数据跳过中间层,直接从提供者到达消费者。


二、useContext:跨层级读取上下文的利器

1. 核心概念与作用

Context(上下文)是React内置的一套全局数据共享方案,你可以把它理解成组件树范围内的「数据广播站」:

  • 顶层通过 Provider 提供数据
  • 任意层级的后代组件都可以直接「订阅」这份数据
  • 完全不需要经过中间组件层层传递

useContext 则是React专门用来消费Context的Hook,它的核心作用就是:跨层级直接读取组件上下文数据,省去逐层向下传递props的麻烦

2. useContext 标准三步使用法

使用Context实现跨层级通信,固定遵循三个步骤:

步骤 API 作用
① 创建上下文 createContext(默认值) 创建一个上下文对象,默认值作为兜底
② 提供上下文 <Context.Provider value={数据}> 包裹组件树,通过 value 注入共享数据
③ 消费上下文 useContext(Context) 在任意深层组件中直接读取共享数据

3. 实战案例:主题切换功能

我们以全局主题切换为例,完整走一遍Context的落地流程。

第一步:创建Theme上下文

新建 ThemeContext.js 文件,创建并导出上下文对象,同时设置默认兜底值:

jsx 复制代码
import { createContext } from 'react'

// 创建主题上下文,为深层组件树提供主题数据
// 默认值仅作为未匹配到Provider时的降级兜底
export const ThemeContext = createContext({
    theme:'light',
    setTheme:()=>{}
});

export default ThemeContext;

💡 亮点解读 :默认值里放一个空函数 setTheme: () => {},是为了保证消费方拿到的对象「形状完整」,而不是 undefined。在JS里这是防崩,在TS里这直接决定了类型推断是否顺畅。

第二步:顶层Provider提供数据

App.jsx 中用Provider包裹组件树,把主题状态和切换方法注入上下文:

jsx 复制代码
import Page from './components/Page'
import { ThemeContext } from './ThemeContext'
import { useState } from 'react'

function App() {
  const [theme, setTheme] = useState('light')
  
  return (
    // 上下文提供者容器
    // Provider不一定非要放在全局根组件,可以放在任何需要的层级作为容器
    // 默认值light可以通过value属性覆盖为真正的响应式数据
    <ThemeContext.Provider value={theme}>
      <Page />
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        切换主题
      </button>
    </ThemeContext.Provider>
  )
}

export default App;

💡 亮点解读 :很多人一听Context就以为「必须包在最外层」,其实不然。Provider可以放在组件树的任意位置 ,它影响的范围就是它的子树。这意味着你可以做局部作用域Context ------比如只给某个复杂表单区域包一个 FormContext.Provider,表单外的组件根本感知不到它,天然做了隔离。

第三步:深层组件直接消费

Page 组件和更深层的 Child 组件中,不需要接收任何props,直接通过 useContext 就能拿到主题数据:

jsx 复制代码
// Page组件
import Child from "../components/Child";
import { useContext } from "react";
import { ThemeContext } from "../ThemeContext";

const Page = () => {
    const theme = useContext(ThemeContext)
    return (
      <>
        <h1>Page {theme}</h1> 
        <br />
        <Child />
      </>
    )
}

export default Page;
jsx 复制代码
// Child组件(更深层级)
import { useContext } from "react";
import { ThemeContext } from "../ThemeContext";

function Child() {
  const theme = useContext(ThemeContext)
  return (
    <>
      <h1>Child</h1>
      <button className="theme">按钮{theme}</button>
    </>
  )
}

export default Child;

🖼️ 配图提示词:手绘风格React Context数据流示意图,顶部是ThemeContext.Provider数据提供者,向下发散箭头直接指向深层的Page组件和Child组件,跳过中间层级,标注「跨层级直接共享」,16:9比例,卡通技术插画风格。

4. 进阶优化:把Context消费封装成自定义Hook

上面的写法虽然能用,但每个消费组件都要同时引入 useContextThemeContext,重复代码多、耦合度高。我们可以把消费逻辑进一步封装成一个自定义Hook------useTheme

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

// 约定以use开头,是自定义Hook的标识
export const useTheme = () => {
    return useContext(ThemeContext)
}

封装之后,组件里使用就变得极其简洁:

jsx 复制代码
const theme = useTheme()

这一步封装的好处远不止少写两行代码:

  • 屏蔽底层实现:组件不需要知道Context的存在,只需要调用Hook拿数据
  • 统一维护:后续如果Context改名、拆分,只需要修改这一个Hook
  • 扩展能力:后续可以在Hook里加入计算属性、参数校验等逻辑

💡 亮点解读 :如果哪天你想把Context换成Zustand,只需要改 useTheme 这一个文件,所有组件一行都不用动。这就是架构层面的收口------把变化点集中到一处。

5. Context的重要注意事项(拓展)

Context虽好用,但不能滥用,有几个关键特性需要注意:

  • 重新渲染机制 :当Provider的 value 发生变化时,所有消费该Context的后代组件都会强制重新渲染,不管它们真正用到的是不是变化的那部分。如果共享数据变化频繁,可能会带来性能问题。

    💡 优化技巧 :如果 value 是每次渲染都新建的对象(如 value={{ theme, setTheme }}),所有消费者都会重渲染。可以用 useMemo 包一下:

    jsx 复制代码
    const value = useMemo(() => ({ theme, setTheme }), [theme]);

    更进阶的做法是拆分Context,把「读」和「写」分成两个Context,减少不必要的更新传播。

  • 默认值的作用createContext 的默认值不是「初始值」,只有当组件找不到对应的Provider时才会生效,属于降级兜底方案。

  • 滥用边界:不是所有数据都适合塞进Context。很多新手会陷入「万物皆可 Context」的误区,这里给你两条清晰的判断标准:

    适合放进 Context ✅ 不建议放进 Context ❌
    主题、用户信息、语言包 表单的临时输入值
    路由、全局配置 列表的滚动位置、分页数据
    登录态、权限信息 高频变化的业务状态(如拖拽坐标)

    判断标准就一句话:全局且低频变化的数据 → Context;局部且高频变化的数据 → 组件内部 state。把高频变化的数据塞进 Context,等于让全树消费者陪着你一起重渲染,性能直接崩。

  • 适用场景 :适合存放全局且不频繁变化的数据;如果是复杂高频变化的业务状态,更推荐使用Zustand、Redux等专门的状态管理库。


三、自定义Hook:React逻辑复用的终极方案

1. 什么是自定义Hook?

自定义Hook本质上就是一个use 开头命名的函数,但它和普通函数最大的区别在于:它可以使用React的所有Hook能力(useState、useEffect等),能够持有响应式状态、处理副作用。

简单来说:自定义Hook = 响应式状态 + 副作用逻辑 + 业务逻辑 的封装单元

2. 自定义Hook的三类划分

在往后看案例之前,先建立一个分类心智------自定义Hook按职责通常分为三类,对应不同的复用层级:

类型 代表案例 封装内容 复用范围
上下文消费 Hook useTheme 封装 Context 消费逻辑,屏蔽底层实现 项目内复用
通用副作用 Hook useMouseuseDebounceuseLocalStorage 封装浏览器 API、DOM 操作、副作用与清理 可跨项目复用
业务状态 Hook useTodos 封装特定业务的完整状态逻辑 业务模块内复用

有了这张表,你以后遇到一段可复用的逻辑,第一反应就是:「它属于哪一类?」------能抽象,还要知道抽象成哪一种,才是真正的封装能力。

3. 为什么需要自定义Hook?

自定义Hook是React中逻辑复用的核心手段,它解决了两个核心问题:

  1. 逻辑复用:相同的状态和副作用逻辑,不需要在每个组件里重复编写,一处封装多处使用。
  2. 关注点分离:把业务逻辑、副作用逻辑从UI组件中抽离出来,组件只专注于视图渲染,代码结构更清晰。

和普通的工具函数相比,自定义Hook能完整保留React的响应式特性,是真正的「逻辑级复用」。

4. 自定义Hook的两条核心规则

  • 命名约定 :必须以 use 开头,这是React官方约定,也是语法检查工具判断Hook的依据。
  • 调用规则 :只能在React函数组件的顶层,或者其他自定义Hook中调用,不能在循环、条件判断、普通函数里调用

💡 为什么有这条规则? React内部是用链表 按调用顺序来存储每个Hook状态的。如果你在 if 里调用Hook,两次渲染的调用顺序不一致,React就会「张冠李戴」,把A的状态给到B。这也是为什么 use 前缀如此重要------ESLint插件靠它来帮你检查调用位置是否合法。
💡 还有一个容易误解的点 :自定义Hook复用的是逻辑,不是状态 。两个组件各自调用 useMouse(),它们会各自持有一份独立的 x/y 状态,互不干扰。想共享状态,那该用Context或状态管理库。

5. 实战案例1:useMouse 封装副作用与清理

最经典的自定义Hook场景,就是封装带副作用的DOM事件监听。

比如实现「页面实时显示鼠标坐标」的功能,如果直接写在组件里,是这样的:

jsx 复制代码
const [x, setX] = useState(null);
const [y, setY] = useState(null);

useEffect(() => {
  function handle(e){
    setX(e.clientX);
    setY(e.clientY);
  }
  document.addEventListener('mousemove', handle)
  
  // 重点:事件监听、定时器等副作用,组件卸载后不会主动回收
  // 必须在清理函数中手动移除,否则会造成内存泄漏
  return () => {
    document.removeEventListener('mousemove', handle)
  }
}, [])

把这段逻辑封装到 hooks/useMouse.js 中:

jsx 复制代码
import { useState, useEffect } from 'react'

export const useMouse = () => {
    const [x, setX] = useState(null);
    const [y, setY] = useState(null);

    function handle(e){
      setX(e.clientX);
      setY(e.clientY);
    }

    useEffect(() => {
      document.addEventListener('mousemove', handle)
      return () => {
        document.removeEventListener('mousemove', handle)
      }
    }, [])

    return { x, y }
}

export default useMouse;

封装完成后,组件中使用只需要一行代码:

jsx 复制代码
const { x, y } = useMouse();

使用方完全不需要关心事件绑定、清理这些细节,Hook内部已经全部处理好了。

💡 亮点解读

  • 清理函数绝不能省 :函数组件卸载后,React不会主动回收你手动绑定的东西------定时器、Worker、事件监听都是如此。如果不写清理,组件卸载后 handle 仍然挂在 document 上,setX 会被反复调用,轻则内存泄漏,重则报「更新已卸载组件的state」警告。这是自定义Hook最有价值的地方之一:把「副作用 + 清理」这套固定套路一次性封装好,调用方永远不用担心忘记清理。
  • 返回对象 { x, y } 而不是数组 :返回对象的好处是调用方可以按需解构,顺序无关。数组形式(像 useState 那样)适合「顺序有意义」的场景。

6. 实战案例2:useTodos 封装业务状态逻辑

自定义Hook不仅能封装副作用,更能封装完整的业务逻辑。我们以Todo待办列表为例,结合TypeScript实现一个完整的业务Hook。

第一步:先定义类型

types/todo.ts 中定义数据结构,用TypeScript做类型约束:

ts 复制代码
// interface 适合定义对象结构
export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

// type 适合定义简单类型、联合类型
export type FilterType = 'all' | 'completed' | 'uncompleted';

💡 亮点解读

  • interface 与 type 的分工interface 描述对象结构 ,可以被继承、声明合并;type 更灵活,可以描述联合类型、简单类型别名
  • 联合类型替代枚举 :用 type FilterType = 'all' | 'completed' | 'uncompleted',你在任何地方写 setFilter('complete') 都会直接报错。字符串字面量联合类型是TS里非常好用的一个技巧,比 enum 更轻、更容易序列化。
第二步:封装useTodos业务Hook

hooks/useTodos.ts 中,把待办事项的增、删、切换、清空等所有状态操作全部封装起来:

ts 复制代码
import { useState, useMemo } from 'react'
import type { Todo, FilterType } from '../types/todo'

export function useTodos() {
    const [todos, setTodos] = useState<Todo[]>([]);
    const [filter, setFilter] = useState<FilterType>('all');
    
    // 新增待办
    const addTodo = (text: string) => {
        if(!text.trim()) return;
        const newTodo: Todo = {
            id: Date.now(),
            title: text.trim(),
            completed: false
        }
        // 函数式更新,基于前一个状态生成新状态
        setTodos(prev => [...prev, newTodo]);
    }

    // 切换完成状态
    const toggleTodo = (id: number) => {
        setTodos(prev => prev.map(item =>
            item.id === id
                ? { ...item, completed: !item.completed }
                : item
        ));
    }

    // 删除待办
    const deleteTodo = (id: number) => {
        setTodos(prev => prev.filter(item => item.id !== id));
    }

    // 清空已完成
    const clearCompleted = () => {
        setTodos(prev => prev.filter(item => !item.completed));
    }

    // 衍生计算:根据 filter 过滤出最终展示的列表
    // 用 useMemo 缓存,只有 todos 或 filter 变化时才重新计算
    const filteredTodos = useMemo(() => {
        switch (filter) {
            case 'completed':   return todos.filter(t => t.completed);
            case 'uncompleted': return todos.filter(t => !t.completed);
            default:            return todos;
        }
    }, [todos, filter]);
    
    return {
        todos: filteredTodos,  // 直接暴露过滤后的列表,UI 层无脑渲染
        filter,
        setFilter,
        addTodo,
        toggleTodo,
        deleteTodo,
        clearCompleted
    }
}

这样封装之后,UI组件只需要调用 useTodos() 就能拿到所有数据和方法,完全不用关心内部的状态管理细节,组件代码会变得非常干净。

💡 亮点解读(这个文件信息量很大,逐个拆)

  • import type 与普通 import 分开import type 是TS 3.8+的语法,明确表示「只导入类型,不导入运行时代码」。打包时这些导入会被完全擦除,不会产生无用的模块依赖,也避免了循环引用的一些坑。
  • 函数式更新 setTodos(prev => ...) :为什么不直接 setTodos([...todos, newTodo])?因为 todos当前渲染时闭包里的旧值 。如果你在一次事件里连续调用两次 addTodo,两次读到的 todos 可能是同一个旧值,第二次就会覆盖第一次。只要新状态依赖旧状态,就用函数式更新------这是铁律。
  • 不可变更新 { ...item, completed: !item.completed } :React的更新判断依赖引用比较 。直接改 item.completed = true 数组和对象引用都没变,React可能不会重新渲染。展开运算符创建一个新对象,才是正确姿势。
  • useMemo 缓存衍生数据filteredTodos 依赖 todosfilter,用 useMemo 包起来后,只有这两个值变化时才重新计算,避免了每次渲染都跑一遍 filter。注意不要过早优化 ------列表很小的时候,直接 filter 也没问题;当列表变大、渲染频繁时,useMemo 的价值才体现出来。
  • 输入防御 if (!text.trim()) return;trim() 去空格,既防止空内容,也把 text.trim() 作为最终写入的title,避免用户输入一堆空格。这是「入口处做校验」的工程习惯。
  • 返回的是 filteredTodos 而不是原始 todos :这是一个封装决策------把「过滤」这层逻辑藏在 Hook 内部,UI 层只负责渲染最终结果。如果哪天过滤规则要改(比如加个搜索框),只需要改这个 Hook,视图层一行不动。这就是**「逻辑与视图分离」**的极致体现。
    🖼️ 配图提示词:手绘风格自定义Hook封装示意图,左侧是分散在组件里的useState、useEffect、业务逻辑,箭头指向右侧的一个自定义Hook函数盒子,标注「逻辑抽离、复用、封装」,16:9比例,卡通技术插画风格。

7. 自定义Hook 的组合性:Hook 调用 Hook

到这里,你可能已经觉得自定义Hook挺好用了。但它真正甩开 HOC(高阶组件)和 mixin 的地方,其实在组合性 ------自定义Hook 可以调用其他自定义Hook,实现逻辑层层叠加

举个例子。我们希望 useTodos 的数据能持久化到 localStorage,刷新页面不丢。直觉做法是在 useTodos 里写 localStorage.setItem,但这会让「业务逻辑」和「存储逻辑」耦合在一起。

更优雅的做法是:先把「读写 localStorage」封装成一个通用副作用 Hook,然后在 useTodos直接调用它

ts 复制代码
// hooks/useLocalStorage.ts
// 通用副作用 Hook:封装 localStorage 的读写 + 副作用同步
import { useState, useEffect } from 'react'

export function useLocalStorage<T>(key: string, initialValue: T) {
    const [value, setValue] = useState<T>(() => {
        // 初始化时尝试从 localStorage 读取
        const raw = localStorage.getItem(key)
        return raw ? JSON.parse(raw) as T : initialValue
    })

    // value 变化时同步写回 localStorage
    useEffect(() => {
        localStorage.setItem(key, JSON.stringify(value))
    }, [key, value])

    return [value, setValue] as const
}

然后在 useTodos 里组合它:

ts 复制代码
// hooks/useTodos.ts
import { useState, useMemo } from 'react'
import { useLocalStorage } from './useLocalStorage'
import type { Todo, FilterType } from '../types/todo'

export function useTodos() {
    // 🔑 关键:把 useState 换成 useLocalStorage
    const [todos, setTodos] = useLocalStorage<Todo[]>('todos', [])
    const [filter, setFilter] = useState<FilterType>('all')

    // 后续增删改查逻辑完全不变
    const addTodo = (text: string) => { /* ... */ }
    // ...

    return { todos: filteredTodos, filter, setFilter, addTodo, /* ... */ }
}

一行代码的改动useTodos 就拥有了本地持久化能力。业务逻辑完全没动,因为「存储」这件事已经被 useLocalStorage 接管了。

这就是组合性的威力:

  • HOC / mixin 的老问题:多层嵌套、来源不明、props 污染、生命周期互相打架。
  • 自定义Hook 的解法 :逻辑是函数调用,不产生新的组件层级,也不污染 props,来源清清楚楚,调用顺序也一目了然。

💡 心法总结 :写自定义Hook 时,遇到任何「可以独立复用的逻辑」,第一反应都应该是------把它抽成另一个 Hook,然后组合进来 。你的 useTodos 可以组合 useLocalStorageuseDebounceuseRequest;你的 useForm 可以组合 useValidationuseDirtyCheckHook 生态就是这么一层一层长出来的。


四、从Demo到工程化:前端项目的分层架构

当我们把Context、自定义Hook这些知识点落地到真实项目中时,就不能再是文件随意堆放的Demo模式,而是需要清晰的架构分层。

标准的React+TS项目分层

一个工程化的React项目,src目录下通常会按职责拆分为以下目录:

目录 职责 存放内容
types/ 类型层 全局类型定义、interface、type别名,统一数据契约
hooks/ 逻辑层 自定义Hook,按功能拆分,封装状态、副作用、业务逻辑
components/ 视图层 UI组件,只负责视图渲染和交互触发,不包含复杂业务逻辑
api/ 接口层 后端接口请求封装,统一管理请求、响应拦截、错误处理

对应到我们的Todo项目中:

  • types/todo.ts 定义了Todo数据结构和筛选类型
  • hooks/useTodos.ts 封装了待办的全部业务逻辑
  • components/ 下的 TodoInputTodoItemTodoListTodoFilter 只负责UI渲染
  • api/ 存放后续对接后端的增删改查接口

这样分层的核心价值

  1. 职责单一:每个目录只负责一类事情,改类型不会碰组件,改逻辑不会动UI,符合「高内聚、低耦合」原则。
  2. 可维护性强:新人接手项目,通过目录结构就能快速定位代码,降低理解成本。
  3. 可复用性高:hooks和types可以跨组件、跨项目复用,UI组件也可以快速替换。
  4. 可测试性好:业务逻辑都在Hook里,可以单独编写单元测试,不依赖UI渲染。

分层的灵魂:单向依赖原则

上面列的都是「目录怎么分」,但真正让分层从「文件夹分类」升级为「架构」的,是依赖方向 ------所有依赖必须是单向的,只能从上层流向下层,绝不能反向。

markdown 复制代码
components  →  hooks  →  types
     ↓            ↓
    api  ────────┘

具体的规则是:

层级 可以依赖 不可以依赖
types/ 什么都不依赖(最纯净) hooks、components、api
hooks/ types、其他 hooks components、api
api/ types hooks、components
components/ hooks、types、api ---

为什么这条原则这么重要?

  • 避免循环依赖hooks 引用 componentscomponents 又引用 hooks,打包时就会陷入循环,运行时会出现各种诡异 bug。
  • 保护底层稳定types 是最底层,一旦它去依赖 hooks,整个架构就失去了「契约先行」的意义------契约应该稳定,业务逻辑才应该灵活。
  • 便于替换和测试 :底层不依赖上层,意味着底层可以被单独测试、单独替换,你的 typeshooks 才能被真正复用。

💡 一句话记住它依赖只能从上往下,不能从下往上。 违反这条原则的架构,再怎么分类都只是「好看的文件柜」。


五、知识串联与总结

最后我们把整条知识链串起来,形成完整的逻辑闭环:

  1. 我们从最基础的父子组件props传参 起步,遇到了深层嵌套下的 Props Drilling 属性穿透痛点
  2. 第一道防线是组件组合 ------用 children 插槽绕过中间层,让中间组件完全不用感知数据。组合解决不了,再上 Context。
  3. 为了解决组合覆盖不到的场景,我们引入了 Context + useContext,实现了跨层级直接共享数据,免去了层层传递的麻烦。
  4. 为了让Context消费更简洁、更易维护,我们把它封装成了自定义Hook,这是我们第一次尝到逻辑封装的甜头。
  5. 进而我们发现,不仅是Context,DOM副作用、业务状态逻辑 都可以封装成自定义Hook,而自定义Hook本身还能互相组合,实现逻辑层层叠加。
  6. 最后当项目规模变大,我们按照类型层、逻辑层、视图层、接口层 的思路进行架构分层,并遵守单向依赖原则,让代码结构从「能跑」升级为「好维护」。

学习React从来不是死记硬背API,而是要理解每一个API背后解决的痛点,以及如何通过封装、抽象、分层,让代码变得更优雅、更有工程价值。

回顾一下你走过的路:从props传参 → 遇到prop drilling → 先用组件组合绕过中间层 → 组合不行再用Context跨越层级 → 用自定义Hook封装消费逻辑 → 用自定义Hook封装副作用和业务逻辑 → 用Hook组合叠加能力 → 用TS分层架构和单向依赖组织项目。每一步都不是凭空来的,都是被上一步的痛点推着走的。这就是React学习最自然的路线。


🎉 最后的最后

看到这里的你,已经比99%「收藏了就等于学会了」的同学强太多了 👏

如果这篇文章帮你理清了Context和自定义Hook的那点事儿,或者让你对「为什么要这么写」突然有了「啊哈」的时刻------那这篇文章就没白写。

求你三连! 🙏

  • 点个赞 👍 ------ 让更多正在被Prop Drilling折磨的同学看到这篇
  • 收个藏 ⭐ ------ 下次写Context的时候翻出来抄作业
  • 关个注 🔔 ------ 后续还会更新更多React实战踩坑笔记

当然,如果你觉得哪里写得不对、或者你有更骚的封装姿势,评论区狠狠拍我,咱们一起把这个知识点聊透 💬

毕竟------学React最好的方式,从来不是一个人闷头看文档,而是一群人互相卷。 😎

我们下篇文章见!🚀


如果这篇文章对你有帮助,欢迎点赞收藏。有任何疑问或者更好的写法,也欢迎在评论区一起讨论 👇

相关推荐
yume_sibai1 小时前
05-Flutter实战项目
前端·flutter
躺柒1 小时前
读数据架构知识体系指南01关系数据仓库(上)
数据仓库·架构·数据分析·spark·数据湖·关系数据库·企业数据仓库
涛涛ing1 小时前
React 19.3 发布,但真正的赢家是 StyleX:当 AI 成为框架的“第一用户”
前端
zhangminghuan2 小时前
Vue vs React 实战拓展面试题|摒弃基础八股,聚焦业务开发核心难点(2026前端进阶)
前端
IT_陈寒2 小时前
Redis主从切换竟让业务卡了3秒?这个坑我替你踩了
前端·人工智能·后端
程序员清风2 小时前
Java 后端如何接入大语言模型
java·spring boot·架构·aigc
天若有情6732 小时前
独立开发复盘:我用 Node.js 做了个在线工具箱
前端·json·工具
xieliyu.2 小时前
前端基础:常见CSS选择器用法
前端·css·笔记·vscode
zttbee2 小时前
前端测试内容
前端