普通视图

发现新文章,点击刷新页面。
今天 — 2025年7月21日掘金 前端

0基础进大厂,React框架基础篇:创建你的第一个React框架项目——梦开始的地方

2025年7月20日 18:05
引言 作为前端初学者,切了这么多页面,在不知道你是否会有疑问——有很多相同的HTML结构,比如导航栏、侧边栏等待,但是一个页面要有一个HTML文件,但是你知道完全没有必要再写一遍,所以你选择Ctrl+
昨天 — 2025年7月20日掘金 前端

RTK Query 完全指南:简化数据获取与状态管理

作者 土豆1250
2025年7月20日 15:36

什么是 RTK Query?

RTK Query 是 Redux Toolkit 官方提供的数据获取和缓存解决方案。它专为解决现代应用中的数据获取需求而设计,通过自动化缓存管理、数据获取和状态更新,显著减少数据管理所需的样板代码

核心优势

  • ⚡️ 零配置数据缓存:自动管理缓存生命周期
  • 📦 内置加载状态:自动跟踪请求状态(loading/success/error)
  • 🔄 自动数据同步:通过标签系统实现自动刷新
  • 🔌 高度可扩展:轻松与现有 Redux 逻辑集成
  • 🧩 TypeScript 优先:提供完整的类型安全支持

快速入门指南

安装依赖

npm install @reduxjs/toolkit react-redux

基础使用示例

1. 创建 API 服务

// apiSlice.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Post'], // 定义标签类型
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => '/posts',
      providesTags: ['Post'] // 为此查询提供'Post'标签
    }),
    addPost: builder.mutation({
      query: (newPost) => ({
        url: '/posts',
        method: 'POST',
        body: newPost
      }),
      invalidatesTags: ['Post'] // 使所有'Post'标签失效
    })
  })
})

export const { useGetPostsQuery, useAddPostMutation } = apiSlice

2. 配置 Redux Store

// store.js
import { configureStore } from '@reduxjs/toolkit'
import { apiSlice } from './apiSlice'

export const store = configureStore({
  reducer: {
    [apiSlice.reducerPath]: apiSlice.reducer
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(apiSlice.middleware)
})

3. 在组件中使用

// PostsComponent.jsx
import { useGetPostsQuery, useAddPostMutation } from './apiSlice'

function Posts() {
  const { data: posts, isLoading, isError } = useGetPostsQuery()
  const [addPost] = useAddPostMutation()

  const handleSubmit = async () => {
    await addPost({ title: 'New Post', content: 'RTK Query is awesome!' })
  }

  if (isLoading) return <div>Loading...</div>
  if (isError) return <div>Error loading posts</div>

  return (
    <div>
      <button onClick={handleSubmit}>Add Post</button>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  )
}

与 Axios 集成(自定义 baseQuery)

RTK Query 默认使用 fetchBaseQuery(基于 Fetch API),但可以轻松集成 Axios:

// axiosBaseQuery.js
import axios from 'axios';

const axiosBaseQuery = ({ baseUrl } = { baseUrl: '' }) => 
  async ({ url, method, data, params }) => {
    try {
      const result = await axios({ 
        url: baseUrl + url, 
        method, 
        data, 
        params 
      })
      return { data: result.data }
    } catch (axiosError) {
      return {
        error: {
          status: axiosError.response?.status,
          data: axiosError.response?.data || axiosError.message
        }
      }
    }
  }

// 在 createApi 中使用
export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: axiosBaseQuery({ baseUrl: '/api' }),
  // ...其他配置
})

RTK Query 与 Redux 的关系

RTK Query 是 Redux Toolkit 生态系统的一部分:

  • 构建在 Redux 之上:使用 Redux store 存储所有状态
  • 无缝集成:与现有 Redux 逻辑并存,共享同一个 store
  • 自动生成 reducer 和 action:无需手动编写 reducer 逻辑
  • 中间件驱动:使用自定义中间件管理缓存生命周期
graph TD
  A[React组件] --> B[使用 useQuery/useMutation]
  B --> C[RTK Query API]
  C --> D[Redux Store]
  D --> E[自动管理状态]
  E --> F[缓存/加载/错误状态]

自动化缓存管理:标签系统

RTK Query 的标签系统是其最强大的特性之一,通过 providesTagsinvalidatesTags 实现自动缓存失效和重新获取。

标签系统工作原理

  1. 标记数据关系

    endpoints: builder => ({
      getPosts: builder.query({
        query: () => '/posts',
        providesTags: ['Post'] // 此查询提供'Post'标签
      }),
      addPost: builder.mutation({
        query: (post) => ({ /* ... */ }),
        invalidatesTags: ['Post'] // 使所有'Post'标签失效
      })
    })
    
  2. 精确缓存控制(使用 ID):

    getPost: builder.query({
      query: (id) => `/posts/${id}`,
      providesTags: (result, error, id) => 
        [{ type: 'Post', id }]
    }),
    
    updatePost: builder.mutation({
      query: ({ id, ...patch }) => ({
        url: `/posts/${id}`,
        method: 'PATCH',
        body: patch
      }),
      invalidatesTags: (result, error, { id }) => 
        [{ type: 'Post', id }]
    })
    
  3. 组合标签

    getUserPosts: builder.query({
      query: (userId) => `/users/${userId}/posts`,
      providesTags: (result, error, userId) =>
        result 
          ? [
              ...result.map(post => ({ type: 'Post', id: post.id })),
              { type: 'UserPosts', id: userId }
            ]
          : ['UserPosts']
    })
    

当执行变更操作(mutation)时,所有关联的标签会自动失效,触发相关查询的重新获取。

对比其他状态管理方案

特性 RTK Query Redux + Thunk/Saga React Query SWR
学习曲线 中等 (需Redux知识)
缓存管理 自动 手动 自动 自动
数据获取抽象 高级抽象 低级实现 高级抽象 高级抽象
与Redux集成 原生支持 原生支持 需要额外集成 需要额外集成
自动重新获取 通过标签系统 手动实现 通过键/依赖 通过键/依赖
加载状态管理 自动 手动 自动 自动
变更操作 内置mutation 手动实现 内置mutation 需要额外实现

RTK Query 的独特优势

  1. Redux 生态集成

    • 与现有 Redux store 无缝协作
    • 可直接访问 Redux DevTools 调试
    • 与 Redux Thunk/其他中间件兼容
  2. 极少的样板代码

    // 传统 Redux 异步 action
    const fetchPosts = () => (dispatch) => {
      dispatch(postsLoading())
      axios.get('/posts')
        .then(res => dispatch(postsReceived(res.data)))
        .catch(err => dispatch(postsFailed(err.message)))
    }
    
    // RTK Query 等效实现
    getPosts: builder.query({
      query: () => '/posts'
    })
    
  3. 智能缓存策略

    • 相同请求的自动去重
    • 组件卸载后保留数据缓存
    • 窗口重新聚焦时自动刷新
  4. 自动生成的 React Hooks

    • useQuery:处理数据获取
    • useMutation:处理数据变更
    • useLazyQuery:按需触发查询

降低上手门槛的技巧

  1. 利用代码生成:从 OpenAPI/Swagger 规范自动生成 API slice

    npx @rtk-query/codegen-openapi openapi-config.json
    
  2. 从简单场景开始

    // 最小化配置
    export const api = createApi({
      baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
      endpoints: () => ({})
    })
    
  3. 逐步添加标签

    • 开始时可以不使用标签
    • 需要缓存失效时再添加 providesTags/invalidatesTags
  4. 重用查询参数

    // 在多个组件中使用相同查询
    function ComponentA() {
      const { data } = useGetPostsQuery()
      // ...
    }
    
    function ComponentB() {
      // 自动重用缓存数据
      const { data } = useGetPostsQuery()
      // ...
    }
    
  5. 高级场景处理

    // 条件查询
    const { data } = useGetPostQuery(id, {
      skip: !id // 当id不存在时跳过查询
    });
    
    // 轮询查询
    const { data } = useGetPostsQuery(undefined, {
      pollingInterval: 5000 // 每5秒刷新一次
    });
    

总结

RTK Query 彻底改变了 Redux 应用中的数据管理方式:

  • 🚀 减少约 80% 的数据管理样板代码
  • 🔄 通过标签系统实现智能缓存管理
  • ⚙️ 无缝集成现有 Redux 架构
  • 📡 支持多种通信协议(REST/GraphQL/WebSockets)
  • 🧪 提供完整的类型安全(TypeScript)

对于已经在使用 Redux 的项目,RTK Query 提供了最平滑的数据获取升级路径。即使是不熟悉 Redux 的开发者,也能通过其简洁的 API 快速上手高效的数据管理。

开始尝试 RTK Query,体验现代 React 应用数据管理的最佳实践!

官方资源:

针对初学者的JS八种类型实用小技巧总结

2025年7月20日 15:29

一、!! 和 !!! 的深入理解

1. !!(双重非)操作符
将任意值强制转换为布尔类型,等效于 Boolean() 函数。
转换规则

  • 假值nullundefined0''NaNfalse)→ false

  • 其他值 → true(包括空数组[]、空对象{}、函数等)

典型应用场景

javascript

// 判断对象是否存在
const user = null;
console.log(!!user); // false

// 简化条件判断
if (!!items.length) { /* 处理非空数组 */ }

// 在Vue项目中判断数据状态
const isLoggedIn = !!user.token;

2. !!!(三重非)操作符
先通过!!转换为布尔值,再取反一次,等效于 !Boolean(值)
典型应用场景

javascript

// 简化反向逻辑判断
const isEmpty = !!!value; // 等效于 value === null || value === undefined || value === ''

// 在Vue中处理加载状态
loading.value = !!!data; // 数据存在时隐藏加载状态

二、JavaScript 基础实用技巧

1. 空值合并与默认值处理

javascript

// 传统写法(缺陷:0、''、false 也会被替换)
const name = user.name || '默认名称';

// 推荐写法(仅替换 null/undefined)
const name = user.name ?? '默认名称';

// 对象解构默认值
const { age = 18, address = {} } = user;
2. 可选链操作符(Optional Chaining)

javascript

// 传统写法
const city = user && user.address && user.address.city;

// 简洁写法
const city = user?.address?.city;

// 结合空值合并
const city = user?.address?.city ?? '未知城市';
3. 快速数值转换

javascript

const strNum = '123';
const num = +strNum; // 等效于 Number(strNum)

// 取整技巧
const floatNum = 3.14;
const intNum = ~~floatNum; // 双波浪号取整,等效于 Math.floor(3.14)
4. 数组去重

javascript

const arr = [1, 2, 2, 3, 3, 3];
const uniqueArr = [...new Set(arr)]; // [1, 2, 3]
5. 交换变量值

javascript

let a = 1, b = 2;

// 传统写法
const temp = a;
a = b;
b = temp;

// 简洁写法
[a, b] = [b, a];

三、函数与作用域技巧

1. 函数参数默认值

javascript

// 传统写法
function greet(name) {
  name = name || 'Guest';
  console.log(`Hello, ${name}`);
}

// 推荐写法
function greet(name = 'Guest') {
  console.log(`Hello, ${name}`);
}
2. 箭头函数简化

javascript

// 传统函数
const sum = function(a, b) {
  return a + b;
};

// 箭头函数
const sum = (a, b) => a + b;
3. 立即执行函数(IIFE)

javascript

// ES5常用
(function() {
  const privateVar = '私有变量';
  // 私有作用域
})();

// ES6模块替代方案
{
  const privateVar = '私有变量';
  // 块级作用域
}

四、对象与数组操作技巧

1. 对象浅拷贝

javascript

const obj = { a: 1, b: 2 };
const clone = { ...obj }; // 展开语法
// 等效于 Object.assign({}, obj)
2. 数组合并

javascript

const arr1 = [1, 2];
const arr2 = [3, 4];

const merged = [...arr1, ...arr2]; // [1, 2, 3, 4]
3. 数组过滤与映射

javascript

const numbers = [1, 2, 3, 4, 5];

// 过滤偶数并翻倍
const result = numbers
  .filter(n => n % 2 === 0) // [2, 4]
  .map(n => n * 2); // [4, 8]
4. 解构赋值高级用法

javascript

// 对象解构重命名
const { name: userName, age: userAge } = user;

// 数组解构
const [first, second, ...rest] = [1, 2, 3, 4, 5];

五、异步编程技巧

1. 异步函数简化

javascript

// 传统Promise
fetchData()
  .then(data => processData(data))
  .catch(error => console.error(error));

// 推荐:async/await
async function fetchAndProcess() {
  try {
    const data = await fetchData();
    const result = processData(data);
  } catch (error) {
    console.error(error);
  }
}
2. 并行请求处理

javascript

// 多个API并行请求
async function fetchAll() {
  const [user, posts] = await Promise.all([
    fetchUser(),
    fetchPosts()
  ]);
  return { user, posts };
}
3. 防抖与节流

javascript

// 防抖函数(避免频繁触发)
const debounce = (fn, delay) => {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
};

// 节流函数(限制执行频率)
const throttle = (fn, limit) => {
  let inThrottle;
  return (...args) => {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
};

六、性能优化技巧

1. 延迟加载(懒加载)

javascript

// 按需加载模块
const loadHeavyModule = async () => {
  const heavyModule = await import('./heavy-module.js');
  heavyModule.init();
};

// 点击按钮时加载
button.addEventListener('click', loadHeavyModule);
2. 循环优化

javascript

// 传统for循环(性能最优)
for (let i = 0, len = arr.length; i < len; i++) {
  // ...
}

// 避免使用for...in遍历数组(性能较差)
3. 事件委托

javascript

// 父元素监听,子元素触发
parentElement.addEventListener('click', (e) => {
  if (e.target.matches('.child-element')) {
    // 处理子元素点击事件
  }
});

七、调试与错误处理

1. 控制台美化输出

javascript

// 带颜色的日志
console.log('%c重要信息', 'color: blue; font-weight: bold');

// 表格形式输出
console.table([{ name: '张三', age: 20 }, { name: '李四', age: 25 }]);
2. 错误边界(Error Boundary)

javascript

// 自定义错误捕获函数
window.onerror = function(message, source, lineno, colno, error) {
  // 记录错误信息
  logError({ message, source, lineno, colno, error });
  return true; // 阻止错误冒泡
};
3. 断言(Assertion)

javascript

function assert(condition, message) {
  if (!condition) {
    throw new Error(message || 'Assertion failed');
  }
}

// 使用示例
assert(typeof value === 'number', 'value必须是数字');

八、类型检查与转换

1. 类型安全检查

javascript

// 检查数组
Array.isArray([]); // true

// 检查空对象
const isEmptyObject = obj => 
  obj && typeof obj === 'object' && !Object.keys(obj).length;

// 检查null/undefined
const isNullOrUndefined = val => val == null; // 注意:使用==而非===

2. 安全的类型转换

javascript

// 字符串转数字
const num = parseInt('123', 10); // 第二个参数必须为10

// 安全的JSON解析
const parseJSON = (str) => {
  try {
    return JSON.parse(str);
  } catch (e) {
    return null;
  }
};

vueuse的createReusableTemplate函数实现原理

作者 gaze
2025年7月20日 15:28

使用vueuse的时候发现里面有一个createReusableTemplate的工具函数很有意思

根据文档的介绍,它的使用方式如下:

开发中也许会经常遇到这种情况

<template>
  <dialog v-if="showInDialog">
    <!-- something complex -->
  </dialog>
  <div v-else>
    <!-- something complex -->
  </div>
</template>

我们希望代码尽可能的重用,通常情况下我们会将<!-- something complex -->抽取成组件,但是那样做,在抽取出去的组件中,你就没法访问到当前SFC中的各种变量,而只能通过定义propsemits来实现数据传递,这样会有些繁琐

createReuseableTemplate能够在当前组件的template中直接定义一个可重用的片段,就像这样:

<script setup>
import { createReusableTemplate } from '@vueuse/core'

const [DefineTemplate, ReuseTemplate] = createReusableTemplate()
</script>

<template>
  <DefineTemplate>
    <!-- something complex -->
  </DefineTemplate>

  <dialog v-if="showInDialog">
    <ReuseTemplate />
  </dialog>
  <div v-else>
    <ReuseTemplate />
  </div>
</template>

在这个示例中,<DefineTemplate>中写的代码将不会渲染任何东西,而会渲染在<ReuseTemplate />

不禁惊叹这是什么黑科技,迫切想知道是怎么实现的,于是研究了一下源码

其实实现原理很简单,稍微拆分一下它的代码并手动实现如下:

<script setup>
import { defineComponent } from 'vue';
import { ref } from 'vue'

const msg = ref('Hello World!')

// 首先是可重用的template的定义
// 其实它就是一个Component
const DefineTmpl = defineComponent({
  setup(_, { slots }) {
    // 拿到默认的插槽,console.log打印一下就能看出来,这玩意实际上就是一个渲染函数
    // 而setup方法,是可以返回一个渲染函数的(也就是一个能返回vnode的函数),具体可看vue官方文档:
    // https://cn.vuejs.org/guide/extras/render-function.html#declaring-render-function
    return () => slots.default()
    // 这里甚至可以直接return默认插槽,因为它本身就是一个函数
    // return slots.default
  }
})
</script>

<template>
  <DefineTmpl>
    <h1>{{ msg }}</h1>
    <input v-model="msg" />
  </DefineTmpl>
</template>

以上代码的结果就是:你在DefineTmpl写的任何东西(其实就是默认插槽的内容)都会被原样渲染出来

1.png

其实也就是跟不存在DefineTmpl几乎没有区别:

<script setup>
import { defineComponent } from 'vue';
import { ref } from 'vue'

const msg = ref('Hello World!')
</script>

<template>
  <h1>{{ msg }}</h1>
  <input v-model="msg" />
</template>

那么DefineTmpl存在的意义其实就是拿到默认插槽的内容(也就是你在DefineTmpl里写的任何html),它是一个渲染函数,那就用一个变量来接收它

然后该怎么重用它呢??也就是再定义一个Component,返回DefineTmpl里获取到的默认插槽内容就行了:

<script setup>
import { defineComponent } from 'vue';
import { ref } from 'vue'

const msg = ref('Hello World!')

let renderFn;
const DefineTmpl = defineComponent({
  setup(_, { slots }) {
    renderFn = slots.default
  }
})

const Reuse = defineComponent({
  setup() {
    if (!renderFn) return console.warn('未定义模板!')
    return renderFn
    // 或者 return () => renderFn()
  }
})
</script>

<template>
  <p>DefineTmpl部分(不会渲染任何东西)</p>
  <DefineTmpl>
    <h1>{{ msg }}</h1>
    <input v-model="msg" />
  </DefineTmpl>
  <h2>================分割线================</h2>
  <Reuse />
</template>

然后在页面任意地方写上就可以了,渲染结果:

2.png

如果想传入一些自定义的东西怎么办呢,稍微改造一下Reuse:

const Reuse = defineComponent({
  setup(_, { attrs }) {
    if (!renderFn) {
      console.warn('未定义模板')
    }
    return () => renderFn({ ...attrs }) // 将attrs作为渲染函数的props传入
  }
})

然后在DefineTmpl部分可以这样写:

<template>
  <p>DefineTmpl部分(不会渲染任何东西)</p>
  <DefineTmpl #default="{ data }">
    <h1>{{ msg }}</h1>
    <input v-model="msg" />
    <p>自定义内容:{{ data }}</p>
  </DefineTmpl>

  <h2>================分割线================</h2>

  <Reuse :data="'我是自定义内容'" />
</template>

渲染结果:

3.png

整个封装一下,就是一个简陋版本的createReusableTemplate了(当然比vueuse中简陋很多,不过差不多核心实现原理也就是这样了)

function createMyReusableTemplate() {
  let renderFn;
  const DefineTmpl = defineComponent({
    setup(_, { slots }) {
      renderFn = slots.default
    }
  })

  const Reuse = defineComponent({
    setup(_, { attrs }) {
      if (!renderFn) {
        console.warn('未定义模板')
      }
      return () => renderFn({ ...attrs })
    }
  })
  
  return [DefineTmpl, Reuse]
}

useContext 与 useReducer 的组合使用

作者 红衣信
2025年7月20日 15:24

前言

在讲useReducer之前需要介绍一下useState这个hooks函数。useState用于数据管理,响应式数据管理,允许我们在函数组件中存储状态。

随着应用逐渐复杂,我们经常发现useState在管理复杂的状态逻辑时显得有些力不从心。这时,React为我们提供的另一个更为强大的hook——useReducer——可以帮助我们优雅地处理复杂状态。

useReducer允许我们使用 action 和 reducer 的方式来组织复杂的状态逻辑,使其变得更加清晰和模块化,弥补了useState的局限性。

今天让我们用useReducer+useContext来实现一个todosList功能。

了解一下什么是useReducer?

useReducer

useState相似,useReducer也是 React 的 Hook,而且也只能放在组件最顶层使用。与前者不同的地方在于,它是通过 action 来更新状态的,使状态更新逻辑更具可读性。

useReducer接受三个参数:

  • reducer:用于更新 state 的纯函数。参数为 state 和 action,返回值是更新后的 state。state 与 action 可以是任意合法值。
  • initialArg:用于初始化 state 的任意值。初始值的计算逻辑取决于接下来的 init 参数。
  • 可选参数 init:用于计算初始值的函数。如果存在,使用 init(initialArg) 的执行结果作为初始值,否则使用 initialArg
什么是纯函数?

纯函数(Pure Function)是函数式编程中的一个核心概念,它指的是那些在同样的输入下总是产生相同输出的函数,并且不会产生任何副作用。具体来说,纯函数具有以下两个主要特性:

  1. 确定性:给定相同的输入参数,纯函数总是返回相同的结果。这意味着纯函数的输出仅取决于它的输入参数,不受外部状态(比如全局变量、数据库查询或网络请求等)的影响。
  2. 无副作用:纯函数不会修改任何外部状态或数据,也不会进行任何形式的I/O操作,例如文件系统操作、网络请求或打印到控制台等。它们仅仅根据输入参数计算并返回结果。

useReducer返回两个参数:

useReducer 返回一个由两个值组成的数组:

  1. 当前的 state。初次渲染时,它是 init(initialArg) 或 initialArg (如果没有 init 函数)。
  2. dispatch 函数。用于更新 state 并触发组件的重新渲染。
const [state, dispatch] = useReducer(reducer, initialArg, init?)

当你使用 useReducer 时,useReducer 返回一个数组,第一个元素是当前的状态 (state),第二个元素是一个可以用来触发状态更新的 dispatch 函数。通过解构赋值,我们可以方便地将这两个值分别赋给 statedispatch 变量。

实现todoList

1.创建全局上下文

export const TodoContext = createContext(null);

2.创建自定义hooks函数useTodos ,其主要作用是封装待办事项(todos)的状态管理和操作逻辑,方便在不同组件中复用。

const initialTodos =[
    {
        id:1,
        text:'学习React',
        done:false
    }
]
export function useTodos(initial=initialTodos) {
    const [todos,dispatch] = useReducer(todoReducer,initial)

    const addTodo = text => dispatch({type: 'ADD_TODO',text})
    const toggleTodo =(id)=> dispatch({type: 'TOGGLE_TODO',id})
    const removeTodo =(id)=> dispatch({type: 'REMOVE_TODO',id})

    return {
        todos,
        addTodo,
        toggleTodo,
        removeTodo
    }
}

3.在App.jsx中,用创建好的上下文包裹子组件,这样就不需要传递数据了,value 属性指定了要共享的数据,这里共享的是 todosHook 对象。todosHook 包含当前的待办事项列表 todos 和三个操作函数 addTodo 、 toggleTodo 、 removeTodo 。

function App() {
  const todosHook=useTodos();
  return (
      <TodoContext.Provider value={todosHook}>
         <h1>Todo App</h1>
         <AddTodo />
         <TodoList />
      </TodoContext.Provider>
  )
}

4.创建一个纯函数,用来出来todoReducer用于根据不同的 action 来更新待办事项的状态。reducer 接收两个参数:

  • state :当前的待办事项状态,通常是一个数组。
  • action :一个对象,包含 type 属性(表示操作类型)和其他必要的数据。
function todoReducer(state,action){
    switch(action.type){
        case 'ADD_TODO':
            return [...state,{
                    id:Date.now(),
                    text:action.text,
                    done:false
                }]
        case 'TOGGLE_TODO':
            return state.map(todo =>
                todo.id === action.id ?{...todo,done:!todo.done}:todo
            );
        case 'REMOVE_TODO':
            return state.filter(todo => todo.id !== action.id);
        default:
            return state;
    }
}

5.创建一个自定义hooksuseTodoContext,使用全局上下文.创建这么多自定义hooks函数,就是为了让组件内部更加干净,更好管理。

export function useTodoContext(){
    return useContext(TodoContext);
}

6.完成添加todo的功能

const AddTodo =()=>{
    const [text,setText]=useState('');
    const { addTodo }=useTodoContext();// 跨层级
    const handleSubmit=(e)=>{
        e.preventDefault(); // 阻止表单默认行为
        if(text.trim()){ // 去空格
            addTodo(text.trim()); // 调用addTodo方法添加任务
            setText(''); // 清空输入框
        }
    }
    return (
        <form onSubmit={handleSubmit}>
            <input 
            type="text" 
            value={text}
            onChange={(e)=>{
                setText(e.target.value)
            }}
            />
            <button type="submit">Add</button>
        </form>
    )
}

7.完成展示todoslist的功能

const TodoList = () => {
    const {
        todos,
        toggleTodo,
        removeTodo,
    } = useTodoContext();
    
    return (
        <ul>
            {
                todos.map((todo)=>(
                    <li key={todo.id}>
                        <span
                          onClick={()=>toggleTodo(todo.id)}
                          style={{textDecoration:todo.done ? 'line-through':'none'}}
                        >
                        {todo.text}
                        {/* {localStorage.getItem('text')} */}
                        </span>
                        <button onClick={()=>removeTodo(todo.id)}>remove</button>
                    </li>
                ))
            }
        </ul>
    )
}

总结

  • useReducer 核心 :包含响应式状态管理、使用纯函数 reducer 规定状态改变规则、初始值 initValue 以及通过 dispatch 派发 action 对象。
  • useContext 作用 :用于跨层次共享状态,借助 createContext 创建上下文, Context.Provider 提供状态, useContext 消费状态。
  • 组合使用 :将 useContext 和 useReducer 结合,可实现跨层级的全局状态管理,应用场景包括主题、登录状态、待办事项等。
  • 自定义 Hook :介绍了组件渲染与 Hook 状态管理的关系,以及 hook 分别与 useContext 、 useReducer 组合使用在全局应用级别状态管理中的作用。
❌
❌