Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 19 深入浅出, 构建生产级React应用程序 Learn React 19 with Epic React v2/进行中 #204

Open
WangShuXian6 opened this issue Oct 21, 2024 · 8 comments

Comments

@WangShuXian6
Copy link
Owner

WangShuXian6 commented Oct 21, 2024

React 19 深入浅出, 构建生产级React应用程序 Learn React 19 with Epic React v2

1 React基础
001 介绍
002 在JS中实现Hello World
003 在JS中实现Hello World (1)
004 生成根节点
005 生成根节点 (1)
006 爸爸笑话时间
007 原生React API简介
008 创建React元素
009 创建React元素 (1)
010 嵌套元素
011 嵌套元素 (1)
012 深度嵌套元素
013 深度嵌套元素 (1)
014 爸爸笑话时间 原生React API
015 使用JSX简介
016 编译JSX
017 编译JSX (1)
018 插值
019 插值 (1)
020 Spread props
021 Spread props (1)
022 嵌套JSX
023 嵌套JSX (1)
024 片段
025 片段 (1)
026 爸爸笑话时间 使用JSX
027 自定义组件简介
028 简单函数
029 简单函数 (1)
030 原生API
031 原生API (1)
032 JSX组件
033 JSX组件 (1)
034 Props
035 Props (1)
036 爸爸笑话时间 自定义组件
037 TypeScript简介
038 Props (2)
039 Props (3)
040 类型收窄
041 类型收窄 (1)
042 推导类型
043 推导类型 (1)
044 默认Props
045 默认Props (1)
046 减少重复
047 减少重复 (1)
048 Satisfies
049 Satisfies (1)
050 爸爸笑话时间 TypeScript
051 样式简介
052 样式
053 样式 (1)
054 自定义组件
055 自定义组件 (1)
056 尺寸Props
057 尺寸Props (1)
058 爸爸笑话时间 样式
059 表单简介
060 表单
061 表单 (1)
062 表单操作
063 表单操作 (1)
064 输入类型
065 输入类型 (1)
066 提交
067 提交 (1)
068 表单操作
069 表单操作 (1)
070 爸爸笑话时间 表单
071 输入简介
072 复选框
073 复选框 (1)
074 下拉选择
075 下拉选择 (1)
076 单选按钮
077 单选按钮 (1)
078 隐藏输入
079 隐藏输入 (1)
080 默认值
081 默认值 (1)
082 爸爸笑话时间 输入
083 错误边界简介
084 组合
085 组合 (1)
086 其他错误
087 其他错误 (1)
088 重置
089 重置 (1)
090 爸爸笑话时间 错误边界
091 数组渲染简介
092 Key prop
093 Key prop (1)
094 焦点状态
095 焦点状态 (1)
096 Key重置
097 Key重置 (1)
098 爸爸笑话时间 数组渲染
099 React基础结束

2 React钩子
001 React钩子简介
002 UI状态管理简介
003 useState
004 useState (1)
005 控制输入
006 控制输入 (1)
007 推导状态
008 推导状态 (1)
009 初始化状态
010 初始化状态 (1)
011 初始化回调
012 初始化回调 (1)
013 爸爸笑话时间 UI状态管理
014 副作用简介
015 useEffect
016 useEffect (1)
017 清理副作用
018 清理副作用 (1)
019 爸爸笑话时间 副作用
020 状态提升简介
021 提升状态
022 提升状态 (1)
023 提升更多状态
024 提升更多状态 (1)
025 状态合并
026 状态合并 (1)
027 爸爸笑话时间 状态提升
028 DOM副作用简介
029 Refs
030 Refs (1)
031 依赖项
032 依赖项 (1)
033 原始依赖项
034 原始依赖项 (1)
035 爸爸笑话时间 DOM副作用
036 唯一ID简介
037 useId
038 useId (1)
039 爸爸笑话时间 唯一ID
040 井字棋简介
041 setState回调
042 setState回调 (1)
043 在localStorage中保存状态
044 在localStorage中保存状态 (1)
045 添加游戏历史功能
046 添加游戏历史功能 (1)
047 爸爸笑话时间 井字棋
048 React钩子结束

3 高级React API

  1. 高级React API简介
  2. 高级状态管理简介
  3. 新状态_问题
  4. 新状态_解决方案
  5. 以前的状态_问题
  6. 以前的状态_解决方案
  7. 状态对象_问题
  8. 状态对象_解决方案
  9. 动作函数_问题
  10. 动作函数_解决方案
  11. 传统Reducer_问题
  12. 传统Reducer_解决方案
  13. 现实场景_问题
  14. 现实场景_解决方案
  15. 爸爸笑话时间 高级状态管理
  16. 状态优化简介
  17. 优化状态更新_问题
  18. 优化状态更新_解决方案
  19. 爸爸笑话时间 状态优化
  20. 自定义钩子简介
  21. 钩子函数_问题
  22. 钩子函数_解决方案
  23. useCallback_问题
  24. useCallback_解决方案
  25. 爸爸笑话时间 自定义钩子
  26. 共享上下文简介
  27. 上下文提供者_问题
  28. 上下文提供者_解决方案
  29. 上下文钩子_问题
  30. 上下文钩子_解决方案
  31. 爸爸笑话时间 共享上下文
  32. 门户简介
  33. createPortal_问题
  34. createPortal_解决方案
  35. 爸爸笑话时间 门户
  36. 布局计算简介
  37. useLayoutEffect_问题
  38. useLayoutEffect_解决方案
  39. 爸爸笑话时间 布局计算
  40. 命令式处理简介
  41. useImperativeHandle_问题
  42. useImperativeHandle_解决方案
  43. 爸爸笑话时间 命令式处理
  44. 焦点管理简介
  45. flushSync_问题
  46. flushSync_解决方案
  47. 爸爸笑话时间 焦点管理
  48. 同步外部状态简介
  49. useSyncExternalStore_问题
  50. useSyncExternalStore_解决方案
  51. 创建存储实用程序_问题
  52. 创建存储实用程序_解决方案
  53. 处理服务器渲染_问题
  54. 处理服务器渲染_解决方案
  55. 爸爸笑话时间 同步外部状态
  56. 高级React API结束

4 React Suspense

  1. 数据获取简介
  2. 抛出Promise_问题
  3. 抛出Promise_解决方案
  4. 错误处理_问题
  5. 错误处理_解决方案
  6. 正式状态_问题
  7. 正式状态_解决方案
  8. 实用工具_问题
  9. 实用工具_解决方案
  10. 使用React_问题
  11. 使用React_解决方案
  12. 爸爸笑话时间 数据获取
  13. 动态Promise简介
  14. Promise缓存_问题
  15. Promise缓存_解决方案
  16. useTransition_问题
  17. useTransition_解决方案
  18. 挂起闪烁_问题
  19. 挂起闪烁_解决方案
  20. 爸爸笑话时间 动态Promise
  21. 乐观UI简介
  22. 乐观UI_问题
  23. 乐观UI_解决方案
  24. 表单状态_问题
  25. 表单状态_解决方案
  26. 多步操作_问题
  27. 多步操作_解决方案
  28. 爸爸笑话时间 乐观UI
  29. Suspense图像组件简介
  30. 图像组件_问题
  31. 图像组件_解决方案
  32. 图像错误边界_问题
  33. 图像错误边界_解决方案
  34. Key属性_问题
  35. Key属性_解决方案
  36. 爸爸笑话时间 Suspense图像
  37. 响应式简介
  38. useDeferredValue_问题
  39. useDeferredValue_解决方案
  40. 爸爸笑话时间 响应式
  41. 优化简介
  42. 并行加载_问题
  43. 并行加载_解决方案
  44. 服务器缓存_问题
  45. 服务器缓存_解决方案
  46. 爸爸笑话时间 优化
  47. React Suspense结束

5 高级React模式

  1. 高级React模式简介
  2. 组合简介
  3. 组合和布局组件_问题
  4. 组合和布局组件_解决方案
  5. 爸爸笑话时间 组合
  6. 最新Ref简介
  7. 最新Ref_问题
  8. 最新Ref_解决方案
  9. 爸爸笑话时间 最新Ref
  10. 复合组件简介
  11. 复合组件_问题
  12. 复合组件_解决方案
  13. 复合组件验证_问题
  14. 复合组件验证_解决方案
  15. 爸爸笑话时间 复合组件
  16. 插槽简介
  17. 插槽上下文_问题
  18. 插槽上下文_解决方案
  19. 通用插槽组件_问题
  20. 通用插槽组件_解决方案
  21. 插槽Prop_问题
  22. 插槽Prop_解决方案
  23. 爸爸笑话时间 插槽
  24. Prop集合和Getters简介
  25. Prop集合_问题
  26. Prop集合_解决方案
  27. Prop Getters_问题
  28. Prop Getters_解决方案
  29. 爸爸笑话时间 Prop集合和Getters
  30. 状态初始化器简介
  31. 初始化切换_问题
  32. 初始化切换_解决方案
  33. 稳定性_问题
  34. 稳定性_解决方案
  35. 爸爸笑话时间 状态初始化器
  36. 状态Reducer简介
  37. 状态Reducer_问题
  38. 状态Reducer_解决方案
  39. 默认状态Reducer_问题
  40. 默认状态Reducer_解决方案
  41. 爸爸笑话时间 状态Reducer
  42. 控制属性简介
  43. 控制属性_问题
  44. 控制属性_解决方案
  45. 爸爸笑话时间 控制属性
  46. 高级React模式结束

6 React性能优化

  1. React性能优化简介
  2. 元素优化简介
  3. 重用元素_问题
  4. 重用元素_解决方案
  5. 元素Props_问题
  6. 元素Props_解决方案
  7. 上下文_问题
  8. 上下文_解决方案
  9. 元素记忆化_问题
  10. 元素记忆化_解决方案
  11. 组件记忆化_问题
  12. 组件记忆化_解决方案
  13. 爸爸笑话时间 元素优化
  14. 优化上下文简介
  15. 上下文记忆化_问题
  16. 上下文记忆化_解决方案
  17. 提供者组件_问题
  18. 提供者组件_解决方案
  19. 分割上下文_问题
  20. 分割上下文_解决方案
  21. 爸爸笑话时间 优化上下文
  22. 并发渲染简介
  23. useDeferredValue + memo_问题
  24. useDeferredValue + memo_解决方案
  25. 爸爸笑话时间 并发渲染
  26. 代码拆分简介
  27. lazy_问题
  28. lazy_解决方案
  29. 预加载_问题
  30. 预加载_解决方案
  31. 过渡_问题
  32. 过渡_解决方案
  33. 爸爸笑话时间 代码拆分
  34. 高耗计算简介
  35. useMemo_问题
  36. useMemo_解决方案
  37. Web Worker_问题
  38. Web Worker_解决方案
  39. 异步结果_问题
  40. 异步结果_解决方案
  41. 爸爸笑话时间 高耗计算
  42. 渲染优化简介
  43. 组件记忆化_问题
  44. 组件记忆化_解决方案
  45. 自定义比较器_问题
  46. 自定义比较器_解决方案
  47. 原始类型_问题
  48. 原始类型_解决方案
  49. 爸爸笑话时间 渲染优化
  50. 窗口化简介
  51. Virtualizer_问题
  52. Virtualizer_解决方案
  53. 爸爸笑话时间 窗口化
  54. React性能优化结束

7 React服务器组件

  1. React服务器组件简介
  2. 热身简介
  3. 静态React应用_问题
  4. 静态React应用_解决方案
  5. 爸爸笑话时间 热身
  6. 服务器组件简介
  7. 服务器组件_问题
  8. 异步组件_问题
  9. 异步组件_解决方案
  10. 流式传输_问题
  11. 流式传输_解决方案
  12. 服务器上下文_问题
  13. 服务器上下文_解决方案
  14. 爸爸笑话时间 服务器组件
  15. 客户端组件简介
  16. Node.js加载器_问题
  17. Node.js加载器_解决方案
  18. 模块解析_问题
  19. 模块解析_解决方案
  20. 爸爸笑话时间 客户端组件
  21. 客户端路由简介
  22. 客户端路由_问题
  23. 客户端路由_解决方案
  24. 挂起UI_问题
  25. 挂起UI_解决方案
  26. 竞争条件_问题
  27. 竞争条件_解决方案
  28. 历史_问题
  29. 历史_解决方案
  30. 缓存_问题
  31. 缓存_解决方案
  32. 爸爸笑话时间 客户端路由
  33. 服务器操作简介
  34. 操作引用_问题
  35. 操作引用_解决方案
  36. 客户端_问题
  37. 客户端_解决方案
  38. 服务器_问题
  39. 服务器_解决方案
  40. 重新验证_问题
  41. 重新验证_解决方案
  42. 历史重新验证_问题
  43. 历史重新验证_解决方案
  44. 爸爸笑话时间 服务器操作
  45. React服务器组件结束

8 额外:专家访谈

  1. 与Aakansha Doshi讨论如何进入开源
  2. Aurora Scharff讲解如何使用React 19增强表单
  3. Jenna Smith谈AI、Radix构建和Tokenami
  4. Evan Bacon将React服务器组件引入React Native
  5. Kateryna Porshnieva讲解如何使用React 19构建无障碍应用
  6. Lee Robinson讲述React的演变:过去、现在与未来
  7. Matt Brophy谈Remix、React Router和开源
  8. Michelle Beckles谈社区建设和开发者健康
  9. Rick Hanlon讲解React 19的内幕
  10. Sam Selikoff谈React在现代Web开发中的影响
  11. Lydia Hallie讨论JavaScript、React和Web开发的未来
  12. Sebastian Silbermann讲解React 19的测试、工具和过渡
  13. Shruti Kapoor讲述现代Web开发中的无障碍性重要性
  14. Sunil Pai谈如何通过强大的软件、PartyKit和耐久对象改变生活
  15. Theo Browne谈他的个人Web开发经验
  16. Dominik Dorfmeister谈他的开源之旅

1 React Fundamentals

001 Intro
002 Hello World in JS
003 Hello World in JS (1)
004 Generate the Root Node
005 Generate the Root Node (1)
006 Dad Joke Break
007 Intro to Raw React APIs
008 Create React Elements
009 Create React Elements (1)
010 Nesting Elements
011 Nesting Elements (1)
012 Deep Nesting Elements
013 Deep Nesting Elements (1)
014 Dad Joke Break Raw React APIs
015 Intro to Using JSX
016 Compiling JSX
017 Compiling JSX (1)
018 Interpolation
019 Interpolation (1)
020 Spread props
021 Spread props (1)
022 Nesting JSX
023 Nesting JSX (1)
024 Fragments
025 Fragments (1)
026 Dad Joke Break Using JSX
027 Intro to Custom Components
028 Simple Function
029 Simple Function (1)
030 Raw API
031 Raw API (1)
032 JSX Components
033 JSX Components (1)
034 Props
035 Props (1)
036 Dad Joke Break Custom Components
037 Intro to TypeScript
038 Props (2)
039 Props (3)
040 Narrow Types
041 Narrow Types (1)
042 Derive Types
043 Derive Types (1)
044 Default Props
045 Default Props (1)
046 Reduce Duplication
047 Reduce Duplication (1)
048 Satisfies
049 Satisfies (1)
050 Dad Joke Break TypeScript
051 Intro to Styling
052 Styling
053 Styling (1)
054 Custom Component
055 Custom Component (1)
056 Size Prop
057 Size Prop (1)
058 Dad Joke Break Styling
059 Intro to Forms
060 Form
061 Form (1)
062 Form Action
063 Form Action (1)
064 Input Types
065 Input Types (1)
066 Submission
067 Submission (1)
068 Form Actions
069 Form Actions (1)
070 Dad Joke Break Forms
071 Intro to Inputs
072 Checkbox
073 Checkbox (1)
074 Select
075 Select (1)
076 Radios
077 Radios (1)
078 Hidden Inputs
079 Hidden Inputs (1)
080 Default Value
081 Default Value (1)
082 Dad Joke Break Inputs
083 Intro to Error Boundaries
084 Composition
085 Composition (1)
086 Other Errors
087 Other Errors (1)
088 Reset
089 Reset (1)
090 Dad Joke Break Error Boundaries
091 Intro to Rendering Arrays
092 Key prop
093 Key prop (1)
094 Focus State
095 Focus State (1)
096 Key Reset
097 Key Reset (1)
098 Dad Joke Break Rendering Arrays
099 Outro to React Fundamentals

2 React Hooks
001 React Hooks Intro
002 Intro to Managing UI State
003 useState
004 useState (1)
005 Controlling Inputs
006 Controlling Inputs (1)
007 Derive State
008 Derive State (1)
009 Initialize State
010 Initialize State (1)
011 Init Callback
012 Init Callback (1)
013 Dad Joke Break Managing UI State
014 Intro to Side-Effects
015 useEffect
016 useEffect (1)
017 Effect Cleanup
018 Effect Cleanup (1)
019 Dad Joke Break Side-Effects
020 Intro to Lifting State
021 Lift State
022 Lift State (1)
023 Lift More State
024 Lift More State (1)
025 Colocate State
026 Colocate State (1)
027 Dad Joke Break Lifting State
028 Intro to DOM Side-Effects
029 Refs
030 Refs (1)
031 Dependencies
032 Dependencies (1)
033 Primitive Dependencies
034 Primitive Dependencies (1)
035 Dad Joke Break DOM Side-Effects
036 Intro to Unique IDs
037 useId
038 useId (1)
039 Dad Joke Break Unique IDs
040 Intro to Tic Tac Toe
041 setState callback
042 setState callback (1)
043 Preserve State in localStorage
044 Preserve State in localStorage (1)
045 Add Game History Feature
046 Add Game History Feature (1)
047 Dad Joke Break Tic Tac Toe
048 Outro to React Hooks

3 Advanced React APIs

  1. Advanced React APIs Intro
  2. Intro to Advanced State Management
  3. New State _ Problem
  4. New State _ solution
  5. Previous State _ Problem
  6. Previous State _ solution
  7. State Object _ Problem
  8. State Object _ solution
  9. Action Function _ Problem
  10. Action Function _ solution
  11. Traditional Reducer _ Problem
  12. Traditional Reducer _ solution
  13. Real World _Problem
  14. Real World _ solution
  15. Dad Joke Break Advanced State Management
  16. Intro to State Optimization
  17. Optimize state updates _ Problem
  18. Optimize state updates _solution
  19. Dad Joke Break State Optimization
  20. Intro to Custom Hooks
  21. Hook Function _Problem
  22. Hook Function _ solution
  23. useCallback _ Problem
  24. useCallback _ solution
  25. Dad Joke Break Custom Hooks
  26. Intro to Shared Context
  27. Context Provider _ Problem
  28. Context Provider _ solution
  29. Context Hook _ Problem
  30. Context Hook _ solution
  31. Dad Joke Break Shared Context
  32. Intro to Portals
  33. createPortal _ Problem
  34. createPortal _ solution
  35. Dad Joke Break Portals
  36. Intro to Layout Computation
  37. useLayoutEffect _Problem
  38. useLayoutEffect _solution
  39. Dad Joke Break Layout Computation
  40. Intro to Imperative Handles
  41. useImperativeHandle _Problem
  42. useImperativeHandle _ solution
  43. Dad Joke Break Imperative Handles
  44. Intro to Focus Management
  45. flushSync _Problem
  46. flushSync _solution
  47. Dad Joke Break Focus Management
  48. Intro to Sync External State
  49. useSyncExternalStore _Problem
  50. useSyncExternalStore _solution
  51. Make Store Utility _Problem
  52. Make Store Utility _solution
  53. Handling Server Rendering _Problem
  54. Handling Server Rendering _solution
  55. Dad Joke Break Sync External State
  56. Outro to Advanced React APIs

4 React Suspense

  1. Intro to Data fetching
  2. Throwing Promises _Problem
  3. Throwing Promises _Solution
  4. Error Handling _Problem
  5. Error Handling _ Solution
  6. Formal Status _Problem
  7. Formal Status _Solution
  8. Utility _ Problem
  9. Utility _Solution
  10. use React _Problem
  11. use React _Solution
  12. Dad Joke Break Data fetching
  13. Intro to Dynamic Promises
  14. Promise Cache _Problem
  15. Promise Cache _Solution
  16. useTransition _Problem
  17. useTransition _Solution
  18. Pending Flash _Problem
  19. Pending Flash _Solution
  20. Dad Joke Break Dynamic Promises
  21. Intro to Optimistic UI
  22. Optimistic UI _Problem
  23. Optimistic UI _Solution
  24. Form Status _ Problem
  25. Form Status _Solution
  26. Multi-step Actions _Problem
  27. Multi-step Actions _ Solution
  28. Dad Joke Break Optimistic UI
  29. Intro to Suspense img
  30. Img Component _Problem
  31. Img Component _ Solution
  32. Img Error Boundary _Problem
  33. Img Error Boundary _Solution
  34. Key prop _Problem
  35. Key prop _Solution
  36. Dad Joke Break Suspense img
  37. Intro to Responsive
  38. useDeferredValue _Problem
  39. useDeferredValue _Solution
  40. Dad Joke Break Responsive
  41. Intro to Optimizations
  42. Parallel Loading _Problem
  43. Parallel Loading _Solution
  44. Server Cache _Problem
  45. Server Cache _Solution
  46. Dad Joke Break Optimizations
  47. Outro to React Suspense

5 Advanced React Patterns

  1. Advanced React Patterns Intro
  2. Intro to Composition
  3. Composition and Layout Components _ Problem
  4. Composition and Layout Components _Solution
  5. Dad Joke Break Composition
  6. Intro to Latest Ref
  7. Latest Ref _ Problem
  8. Latest Ref _Solution
  9. Dad Joke Break Latest Ref
  10. Intro to Compound Components
  11. Compound Components _ Problem
  12. Compound Components _ Solution
  13. Compound Components Validation _ Problem
  14. Compound Components Validation _Solution
  15. Dad Joke Break Compound Components
  16. Intro to Slots
  17. Slot Context _Problem
  18. Slot Context _ Solution
  19. Generic Slot Components _ Problem
  20. Generic Slot Components _ Solution
  21. Slot Prop _Problem
  22. Slot Prop _ Solution
  23. Dad Joke Break Slots
  24. Intro to Prop Collections and Getters
  25. Prop Collections _Problem
  26. Prop Collections _Solution
  27. Prop Getters _Problem
  28. Prop Getters _Solution
  29. Dad Joke Break Prop Collections and Getters
  30. Intro to State Initializer
  31. Initialize Toggle _Problem
  32. Initialize Toggle _Solution
  33. Stability _Problem
  34. Stability _Solution
  35. Dad Joke Break State Initializer
  36. Intro to State Reducer
  37. State Reducer _Problem
  38. State Reducer _Solution
  39. Default State Reducer _Problem
  40. Default State Reducer _Solution
  41. Dad Joke Break State Reducer
  42. Intro to Control Props
  43. Control Props _Problem
  44. Control Props _Solution
  45. Dad Joke Break Control Props
  46. Outro to Advanced React Patterns

6 React Performance

  1. React Performance Intro
  2. Intro to Element Optimization
  3. Reusing Elements _Problem
  4. Reusing Elements _Solution
  5. Element Props _Problem
  6. Element Props _ Solution
  7. Context _ Problem
  8. Context _Solution
  9. Memoize Elements _Problem
  10. Memoize Elements _Solution
  11. Memoize Components _ Problem
  12. Memoize Components _Solution
  13. Dad Joke Break Element Optimization
  14. Intro to Optimize Context
  15. Memoize Context _Problem
  16. Memoize Context _Solution
  17. Provider Component _Problem
  18. Provider Component _Solution
  19. Split Context _Problem
  20. Split Context _Solution
  21. Dad Joke Break Optimize Context
  22. Concurrent Rendering Intro
  23. useDeferredValue + memo _Problem
  24. useDeferredValue + memo _Solution
  25. Dad Joke Break Concurrent Rendering
  26. Intro to Code Splitting
  27. lazy _ Problem
  28. lazy _Solution
  29. Eager Loading _Problem
  30. Eager Loading _Solution
  31. Transitions _Problem
  32. Transitions _ Solution
  33. Dad Joke Break Code Splitting
  34. Intro to Expensive Calculations
  35. useMemo _Problem
  36. useMemo _Solution
  37. Web Worker _Problem
  38. Web Worker _Solution
  39. Async Results _Problem
  40. Async Results _Solution
  41. Dad Joke Break Expensive Calculations
  42. Intro to Optimize Rendering
  43. Component Memoization _ Problem
  44. Component Memoization _Solution
  45. Custom Comparator _ Problem
  46. Custom Comparator _Solution
  47. Primitives _Problem
  48. Primitives _Solution
  49. Dad Joke Break Optimize Rendering
  50. Intro to Windowing
  51. Virtualizer _Problem
  52. Virtualizer _Solution
  53. Dad Joke Break Windowing
  54. Outro to React Performance

7 React Server Components

  1. React Server Components Intro
  2. Intro to Warm Up
  3. Static React App _Problem
  4. Static React App _Solution
  5. Dad Joke Break Warm Up
  6. Intro to Server Components
  7. RSCs _Problem
  8. Async Components _ Problem
  9. Async Components _Solution
  10. Streaming _Problem
  11. Streaming _ Solution
  12. Server Context _Problem
  13. Server Context _Solution
  14. Dad Joke Break Server Components
  15. Intro to Client Components
  16. Node.js Loader _Problem
  17. Node.js Loader _Solution
  18. Module Resolution _Problem
  19. Module Resolution _Solution
  20. Dad Joke Break Client Components
  21. Intro to Client Router
  22. Client Router _Problem
  23. Client Router _Solution
  24. Pending UI _Problem
  25. Pending UI _Solution
  26. Race Conditions _Problem
  27. Race Conditions _Solution
  28. History _Problem
  29. History _Solution
  30. Cache _Problem
  31. Cache _Solution
  32. Dad Joke Break Client Router
  33. Intro to Server Actions
  34. Action Reference _ Problem
  35. Action Reference _Solution
  36. Client Side _Problem
  37. Client Side _ Solution
  38. Server Side _Problem
  39. Server Side _Solution
  40. Revalidation _Problem
  41. Revalidation _Solution
  42. History Revalidation _ Problem
  43. History Revalidation _Solution
  44. Dad Joke Break Server Actions
  45. Outro to React Server Components

8 Bonus. Interviews With Experts

  1. Getting into Open Source with Aakansha Doshi
  2. Enhancing Forms using React 19 with Aurora Scharff
  3. Jenna Smith on AI, Building Radix, and Tokenami
  4. Evan Bacon brings React Server Components to React Native
  5. Kateryna Porshnieva on Building Accessible Apps with React 19
  6. React's Evolution_ Past, Present, and Future with Lee Robinson
  7. Matt Brophy on Remix, React Router, and Open-Source
  8. Michelle Beckles on Community Building and Developer Health
  9. Under the Hood of React 19 with Rick Hanlon
  10. Sam Selikoff on React's Impact in Modern Web Development
  11. Lydia Hallie on JavaScript, React, and the Future of Web Development
  12. Sebastian Silbermann on Testing, Tooling, and Transitions With React 19
  13. The Importance of Accessibility in Modern Web Development With Shruti Kapoor
  14. Sunil Pai on Changing Lives with Powerful Software, PartyKit, and Durable Objects
  15. Theo Browne on His Personal Experience as a Web Developer
  16. Dominik Dorfmeister on His Open-Source Journey

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Oct 21, 2024

1 React基础

001 介绍

使用 HTML 和 JavaScript 实现 Hello World

要在 HTML 中实现一个 "Hello World",其实并不复杂。你只需要创建一个简单的 HTML 文档,使用 <body> 标签和一个包含 “Hello World” 的 <div> 或者 <p> 标签。

但如果你想通过 JavaScript 实现 "Hello World",也不会特别复杂。这是你第一次接触如何使用 JavaScript 与页面互动,并创建动态和交互式用户体验的入口。

要在页面上添加 JavaScript,可以使用 <script> 标签。你可以直接在这个标签内编写 JavaScript 代码,或者使用 src 属性链接到一个外部的 JavaScript 文件。虽然我们通常会将 JavaScript 代码放在单独的文件中,但为了简单起见,在初步学习中,我们会将所有内容放在一个文件里。

学习目标

在这个练习中,我们的重点是学习以下内容:

  1. 文档对象模型(DOM):JavaScript 如何通过 DOM 操作页面。
  2. 创建与附加元素:如何使用 JavaScript 在页面上创建和附加 HTML 元素。

可以通过访问 MDN DOM 文档 来了解更多关于 DOM 的信息。DOM 是从 JavaScript 在 Web 诞生之初就存在的一部分,通过 DOM,我们能够与页面进行交互。

尽管直接操作 DOM 可能有点混乱,但现代 JavaScript 和 DOM 的操作已经变得非常简洁高效。你可以使用这些技术构建一些不错的用户体验。当然,像 React 这样的框架提供了更多的功能,帮助我们更方便地进行复杂应用的开发,但在这个练习中,我们的重点是了解 DOM 的基础知识。

练习目标

通过本次练习,你将学会使用 JavaScript 直接在 DOM 上实现 "Hello World",为之后深入学习 React 和其他高级工具打下坚实的基础。

002 在JS中实现Hello World

第一步:使用 JavaScript 和 HTML 创建 Hello World

在本次练习的第一步中,我们将使用 JavaScript 在 HTML 中创建一个 "Hello World"。当你完成后,页面应简单显示 "Hello World",如同示例中那样。

你可以按照以下步骤进行操作:

  1. 创建一个基本的 HTML 页面,如 index.html
  2. 在页面中使用 <script> 标签,编写或引用 JavaScript 代码,将 "Hello World" 添加到页面。
  3. 确保页面在浏览器中运行时,能够显示 "Hello World"。

这是一项非常基础的练习,指引会帮助你完成,祝你玩得开心!

003 在JS中实现Hello World (1)

这个视频讲解了如何使用JavaScript和DOM API在HTML中创建一个“Hello World”的示例。以下是关键步骤的总结:

  1. 基本HTML结构

    • 创建一个包含ID为“root”的div元素的HTML文件。
    • 在文件中加入一个<script>标签,并将type设为module,用来嵌入JavaScript代码。
  2. 使用JavaScript操作DOM

    • <script>标签中,使用document.getElementById('root')选择页面上的root元素。
    • 使用document.createElement('div')创建一个新的div元素。
    • 给新创建的div元素添加一个className(例如:container),并设置其textContent为“Hello World”。
    • 使用appendChild()append()将新创建的div元素追加到root元素中。
  3. DOM操作

    • 该脚本展示了如何直接操作DOM,并解释了在内存中创建一个元素并不会自动显示在页面上。
    • 要显示这个元素,必须显式地将其追加到已经存在于文档中的某个元素上(在这个例子中就是root元素)。

通过这些步骤,你可以使用JavaScript和DOM操作技术,动态地在网页上创建并追加元素,实现“Hello World”的显示。

004 生成根节点

这段视频介绍了一个新的练习,目的是让你更深入地理解如何使用JavaScript操作DOM。与之前的练习不同,这次我们将完全用JavaScript生成根元素,而不是依赖于HTML预先定义的元素。以下是视频的主要内容:

  1. 挑战的目标

    • 通过JavaScript生成一个根元素(如<div>),而不是在HTML中预先定义。
    • 进一步推动你对DOM操作的理解和实践。
  2. 任务内容

    • 使用document.createElement动态生成一个根元素,并将其插入到文档中。
    • 通过JavaScript为页面添加内容,而不是依赖HTML。
  3. 动手实验

    • 该练习旨在让你更熟悉JavaScript操作DOM的能力,提升你在前端开发中的实践技能。

练习的核心是完全通过JavaScript操控页面结构,摆脱对HTML的依赖,增强对动态DOM操作的理解。

005 生成根节点 (1)

在这段视频中,主要讲解了如何使用JavaScript动态创建并添加HTML元素到DOM中,以下是视频的核心内容总结:

  1. 删除现有元素

    • 首先,通过删除HTML中的根元素来触发错误。页面将报错,提示“cannot read properties of null reading append”,因为在DOM中找不到该元素。
  2. 使用JavaScript创建元素

    • 使用document.createElement创建一个新的div元素,并设置其id为"root"。
  3. 将新元素添加到DOM中

    • 通过document.body.append方法,将动态创建的div元素(rootElement)添加到body中,使其真正显示在页面上。
    • 如果需要将元素插入到页面的顶部,可以使用document.body.prepend,而不是append
  4. DOM的动态操作

    • 本例展示了如何使用JavaScript动态创建、修改并将元素添加到DOM结构中。这种操作是构建动态Web应用的基础。
    • 进一步扩展,你可以添加事件处理、按钮等,实现更复杂的交互功能。

这段视频的目的是展示如何通过JavaScript操控DOM,创建并修改页面元素,帮助理解如何在动态网页中使用JavaScript进行基本的DOM操作。

006 爸爸笑话时间

在这段视频中,介绍了一个轻松的休息时间。讲者强调了在学习过程中定期休息的重要性,帮助大脑吸收和巩固所学内容。视频还分享了一个轻松的笑话:

  • 笑话内容:迟到的番茄对早到的番茄说了什么?"我会追上你。"("I'll catch up")。

之后,讲者提醒观众保持充足的水分,并鼓励大家利用这段时间稍作休息,为接下来的学习做好准备。

007 原生React API简介

这段视频介绍了如何在引入React的基础上,通过不使用JSX的方式,使用原生React API创建一个简单的"Hello World"应用。讲者首先解释了React是基于传统的document.createElement构建的,目的是让开发者理解React背后的工作原理。以下是视频中的一些关键点:

  1. React与React DOM:讲者介绍了React不仅仅是一个单一的包,它实际上分为两个主要的包:ReactReact DOMReact负责管理组件、hooks和API,而React DOM则负责将React组件渲染到网页的DOM上。

  2. React的跨平台能力:除了网页上的React渲染器(React DOM),React还可以用于虚拟现实、原生桌面应用和命令行界面等场景。

  3. 从原生DOM到React的转换:通过引入React和React DOM,开发者能够将React元素转换为可以在网页上显示的DOM元素。

视频的目的是引导开发者通过逐步学习React的底层API,最终理解React如何简化开发工作,并逐步深入React的使用。

008 创建React元素

在这个练习中,我们将React和React DOM引入页面,并使用它们的createElementcreateRoot API来替代原生的document.createElement等操作。讲者提醒,这并不是通常引入React的方式,通常你会使用构建工具(如Webpack、Parcel)来处理React的构建和优化工作,包括类型检查、打包和性能优化等。

在这个简单的练习中,我们的目标是通过React的声明式API(如React.createElement)来实现之前使用原生DOM API的效果。尽管最后页面的展示效果与之前完全相同,但我们将会使用React来构建整个页面结构。这是从传统DOM操作向React世界过渡的第一步。

你可以通过查看项目仓库中的公共目录来了解如何加载React和React DOM库,并使用它们来创建页面元素。希望你在这次练习中有所收获!

009 创建React元素 (1)

在这个示例中,我们展示了如何使用React和React DOM将元素渲染到页面上。首先,我们通过createElement从React创建一个新的元素,并设置classNamechildren属性来替代传统的classtextContent

React元素只是一个UI描述符,它不是直接的DOM元素。因此,我们还需要通过React DOM中的createRoot API来将这个React元素渲染为实际的DOM元素。以下是我们所做的关键步骤:

  1. 使用createElement:我们用React的createElement来创建一个元素。在这个例子中,我们创建了一个div,并给它设置了classNamechildren属性。
  2. 使用createRoot渲染元素:通过createRoot API,我们将这个元素附加到页面上已经存在的根节点rootElement。然后通过render方法将React元素实际渲染到DOM中。

最后的效果是在页面上显示一个带有"Hello World"文本的div元素。

这是React基本工作原理的一个很好的演示,展示了从UI描述符(React元素)到实际DOM渲染的流程。

010 嵌套元素

在这个练习中,我们将处理多个子元素并确保它们之间的空格正确显示。具体来说,我们会在一个React元素中创建两个span标签,一个用于显示"Hello",另一个用于显示"World",并确保它们之间有一个空格。

目标:

  1. 使用React创建两个span元素,一个显示"Hello",另一个显示"World"。
  2. 确保这两个元素之间有一个空格。

解决方案:

React允许我们将多个子元素传递给一个父元素,而不仅限于单个子元素。要实现这个需求,我们可以通过创建两个span元素并在它们之间添加一个空格或字符串来实现。

示例代码如下:

import React from 'react';
import ReactDOM from 'react-dom/client';

const rootElement = document.getElementById('root');
const root = ReactDOM.createRoot(rootElement);

const element = React.createElement(
  'div', 
  null, 
  React.createElement('span', null, 'Hello'), 
  ' ',  // 这是我们用来添加空格的部分
  React.createElement('span', null, 'World')
);

root.render(element);

说明:

  1. React.createElement: 使用该方法分别创建两个span元素,分别包含"Hello"和"World"。
  2. 空格的处理: 在两个span元素之间直接添加一个字符串' '(一个空格字符),确保在它们之间有空格。

最终效果是页面上显示"Hello World",并且"Hello"和"World"之间有一个空格。

这个练习展示了如何在React中使用多个子元素,并处理它们之间的布局问题。

011 嵌套元素 (1)

在这个练习中,我们使用 React 的 createElement API 来构建嵌套的 UI,并处理多个子元素。这次的目标是创建两个 span 元素,一个显示“Hello”,另一个显示“World”,并确保它们之间有空格。

关键步骤:

  1. 使用 React.createElement 创建 span 元素:

    • 我们将使用 React.createElement('span', null, 'Hello') 创建第一个 span,内容为“Hello”。
    • 然后,创建第二个 span,内容为“World”。
  2. span 元素之间添加空格:

    • 通过直接传递一个空格字符串 ' ',React 将确保这个空格被渲染为文本节点,从而在两个 span 元素之间显示空格。
  3. 将所有元素作为子元素传递:

    • React.createElement 的第三个参数开始表示子元素,可以是文本、元素或数组形式。在这个例子中,我们通过传递多个子元素来构建 div 标签的内容。

以下是完整代码:

import React from 'react';
import ReactDOM from 'react-dom/client';

const rootElement = document.getElementById('root');
const root = ReactDOM.createRoot(rootElement);

const element = React.createElement(
  'div', 
  null, 
  React.createElement('span', null, 'Hello'), 
  ' ',  // 这是用来添加空格的字符串
  React.createElement('span', null, 'World')
);

root.render(element);

解析:

  • React.createElement('span', null, 'Hello'): 创建一个 span 元素,内容为 "Hello"。
  • ' ': 这是一个空格字符串,它被作为子元素传递,使两个 span 元素之间有一个空格。
  • React.createElement('span', null, 'World'): 创建第二个 span 元素,内容为 "World"。

注意点:

  1. React 处理字符串子元素: React 会自动将字符串转换为文本节点并插入到 DOM 中。因此,我们可以简单地传递 ' ' 来表示空格。
  2. 多个子元素: React.createElement 接受多个子元素作为第三个及后续参数。它们会一起渲染到父元素中。

通过这种方式,我们可以确保在页面上正确显示“Hello World”,并且“Hello”和“World”之间有一个空格。

012 深度嵌套元素

在这个练习中,我们将使用 React.createElement 来构建一个更加复杂的嵌套结构。最终的目标是创建一个包含 p 标签、一个 ul 列表及其多个 li 项的结构。当我们完成时,DOM 树应看起来如下:

<div class="container">
  <p>Some text here</p>
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
  </ul>
</div>

任务描述:

  • 使用 createElement 构建整个嵌套结构。
  • 不使用 JSX(虽然这是练习中帮助我们理解为什么 JSX 更简洁的部分)。

代码实现:

import React from 'react';
import ReactDOM from 'react-dom/client';

const rootElement = document.getElementById('root');
const root = ReactDOM.createRoot(rootElement);

// 创建包含嵌套元素的结构
const element = React.createElement(
  'div', 
  { className: 'container' }, // 父 div 容器
  React.createElement('p', null, 'Some text here'), // p 标签
  React.createElement(
    'ul', 
    null, 
    React.createElement('li', null, 'Item 1'), // li 项目1
    React.createElement('li', null, 'Item 2'), // li 项目2
    React.createElement('li', null, 'Item 3')  // li 项目3
  )
);

// 渲染到页面
root.render(element);

解析:

  1. 顶层 div 元素:

    • 使用 React.createElement('div', { className: 'container' }, ...) 创建一个 div,并赋予 className 为 "container"。
  2. p 标签:

    • React.createElement('p', null, 'Some text here') 创建一个 p 标签,包含文本内容 "Some text here"。
  3. ul 列表和 li 项目:

    • React.createElement('ul', null, ...) 创建一个 ul 列表,并在其中嵌套多个 li 项目。
  4. 嵌套结构:

    • ul 列表的每个 li 项是通过调用 React.createElement('li', null, 'Item X') 创建的,X 代表不同的项目编号。

总结:

虽然这个结构不算特别复杂,但通过这种方式嵌套多个 createElement 调用,可以让我们深刻体会到 JSX 的优势。直接使用 createElement 来构建复杂的 UI 结构时,代码的可读性会变差,层次感也不明显。而在 JSX 中,嵌套结构可以以更接近 HTML 的方式书写,既直观又高效。

这个练习让我们理解 React.createElement 的灵活性和强大之处,同时也预示了 JSX 如何简化开发体验。

013 深度嵌套元素 (1)

在这个练习中,我们使用了 React.createElement 来创建更复杂的嵌套 UI 结构。与之前的 Hello World 相比,我们现在正在构建一个包含 <p> 标签、<ul> 列表和多个 <li> 项的结构。这个结构展示了 React 元素嵌套的工作方式,但同时也让我们理解到 JSX 在处理大量嵌套时的优势。

代码实现

import React from 'react';
import ReactDOM from 'react-dom/client';

// 获取 root 元素
const rootElement = document.getElementById('root');
const root = ReactDOM.createRoot(rootElement);

// 使用 createElement 创建嵌套的 UI 结构
const element = React.createElement(
  'div', 
  { className: 'container' }, 
  React.createElement('p', null, "Sam's favorite food"),  // p 标签
  React.createElement(
    'ul', 
    { className: 'sams-food' },  // ul 列表,带有 className 属性
    React.createElement('li', null, 'Green eggs'),  // li 项目1
    React.createElement('li', null, 'Ham')  // li 项目2
  )
);

// 渲染到页面
root.render(element);

解析:

  1. p 标签

    • 使用 React.createElement('p', null, "Sam's favorite food") 创建一个 p 标签,显示文本内容为 "Sam's favorite food"。
  2. ul 列表

    • React.createElement('ul', { className: 'sams-food' }, ...) 创建一个带有 classNamesams-foodul 列表,并在其中嵌套多个 li 项。
  3. li 项目

    • React.createElement('li', null, 'Green eggs')React.createElement('li', null, 'Ham') 分别创建两个列表项,显示 "Green eggs" 和 "Ham"。

总结:

虽然 React.createElement 提供了创建复杂 UI 的能力,但随着嵌套层级增加,代码的可读性和维护性可能会变差。这就是 JSX 存在的主要原因:它让我们用类似 HTML 的语法来编写 React 元素,大大提高了代码的可读性和简洁性。

通过这个练习,你已经学会了如何使用 React.createElement 创建嵌套的 UI 结构,同时也体验到当 UI 复杂度增加时,JSX 的优势会变得非常明显。

014 爸爸笑话时间 原生React API

哈哈,那个笑话真是让人忍俊不禁!抓住一个放了一个,真是个有趣的双关。希望这个小休息能让你放松一下,记得保持水分和适当休息哦!当你准备好了,我们会继续学习更多的 React 知识,深入探索这个强大的框架,掌握更多有趣的技巧!

015 使用JSX简介

JSX 确实为 React 编写用户界面带来了很大的方便,它使得我们可以以接近 HTML 的语法编写代码,而不必通过复杂的 createElement API 一层层嵌套调用。

React 团队创建了 JSX 语法,它本质上是一种让你在 JavaScript 中写 XML 类似的语法,JSX 然后会通过编译器(如 Babel)转换为 React 的 createElement 调用。虽然浏览器本身不理解 JSX 语法,但是通过像 Babel 这样的工具,JSX 可以被编译成常规的 JavaScript,从而在浏览器中运行。

为了保持事情简单,React 的 JSX 编译器 Babel 可以在浏览器中直接运行,因此我们可以避免使用复杂的构建工具。在本次练习中,你将学习如何在浏览器中通过引入 Babel 来编写 JSX 语法,并了解 JSX 是如何转换为 React.createElement 调用的。

在这项练习中,你将体验到 JSX 的强大和简洁性,让编写 UI 变得更加高效流畅。准备好了吗?让我们一起开始学习 JSX 吧!

016 编译JSX

在这个练习中,我们将把 Babel 添加到页面中,并将 createElement 调用转换为 JSX 语法。

首先,我们需要在页面中引入 Babel,这次我们会使用 Babel 的单独版本(Babel standalone),它是一个打包好的单个脚本,可以直接在浏览器中加载并执行。通过这个脚本,Babel 会在页面中查找具有特定 type 类型的 <script> 标签,并对其内容进行编译,最终生成新的脚本标签让浏览器评估执行。

具体步骤如下:

  1. 加载 Babel standalone:

    • 我们将 Babel standalone 文件放在公共目录下。在 HTML 文件中,通过 <script> 标签将其引入页面。
  2. 更新 <script> 标签:

    • 我们需要将使用 React 的 <script> 标签的 type 属性设置为 "text/babel",这会告诉 Babel,它应该编译这个脚本中的 JSX 内容。
  3. 转换 JSX:

    • 将我们当前使用的 createElement API 替换为 JSX 语法。Babel 会自动将 JSX 转换为对应的 React.createElement 调用。

这个过程让你能够直观地了解 JSX 如何转换为常规的 JavaScript 调用,虽然这种方式不常用于生产环境,但它能帮助你在没有构建工具的情况下运行 React 项目。

祝你练习愉快!

017 编译JSX (1)

在这个练习中,我们已经成功地将 Babel 添加到页面中,并且将 createElement 调用转换为了 JSX 语法。通过这个过程,我们学到了几个关键点:

  1. 添加 Babel:
    Babel 是一个编译器,可以将 JSX 代码转换为 JavaScript 代码,让浏览器能够理解和执行。虽然我们在练习中加载了一个较大的 Babel standalone 文件,这种方法不适合生产环境,但它适合我们进行开发和学习。

  2. 使用 JSX 替代 createElement 调用:
    我们将 JSX 替换了原本的 React.createElement 调用。JSX 语法更接近 HTML,简洁直观,可以更容易地编写 React 组件。

  3. Babel 编译:
    Babel 会自动查找类型为 text/babel<script> 标签,编译其中的内容,并将其转化为浏览器可以执行的 JavaScript 代码。在生产环境中,你通常会使用编译工具(如 Webpack 等)来实现这一过程。

  4. 模块导入:
    Babel 编译后的代码依赖于 React.createElement,所以我们需要确保在代码中引入 React。通过 import * as React from 'react';,我们可以确保 React 被正确导入和引用。

  5. JSX 的好处:
    使用 JSX 语法,让我们可以轻松地嵌套和构建复杂的 UI 元素,而不必手动编写繁琐的 createElement 调用。这样可以大大提高开发效率和代码的可读性。

接下来,我们可以继续探索 JSX 的更多特性和强大之处,并且通过这些练习加深对 React 的理解。

018 插值

在这个练习中,我们讨论了插值(Interpolation)的概念,特别是在 JSX 中的应用。在 React 和 JSX 中,插值是将 JavaScript 代码嵌入到 JSX 表达式中的一种方式,这与在模板字符串(Template Literal)中的插值非常类似。

关键点回顾:

  1. 插值的概念
    插值的核心思想是允许在字符串或 JSX 中嵌入 JavaScript 表达式。通过这种方式,我们可以动态地生成内容。例如,使用反引号 (`) 和 ${} 在模板字符串中嵌入变量或表达式,这与在 JSX 中使用花括号 {} 插入 JavaScript 表达式类似。

  2. JSX 中的插值
    在 JSX 中,你可以通过花括号 {} 在 HTML 标签的属性和内容中嵌入 JavaScript 表达式。例如:

    const name = "React";
    return <div className={`container ${name}`}>Hello, {name}!</div>;

    在这个例子中,classNamediv 的内容都使用了插值,将 JavaScript 表达式插入 JSX。

  3. 插值位置

    • 属性:你可以在 JSX 元素的属性中使用插值,例如 className={classNameVariable}
    • 内容:你还可以在元素的内容中插入变量或表达式,例如 <div>{message}</div>,其中 message 是一个变量。
  4. JSX 和 JavaScript 模式的切换
    在 JSX 中使用 {} 插入 JavaScript 表达式相当于在普通 JavaScript 中使用 ${} 插入变量或表达式。花括号 {} 是一种告诉 JSX 解析器 "进入 JavaScript 模式" 的方式,解析器会解析 {} 中的表达式,然后返回其结果并插入到 DOM 中。

总结:

插值是 React 和 JSX 的重要特性,它使得我们可以轻松地将动态数据插入到组件中,使得我们的 UI 更加灵活和可复用。在接下来的练习中,你将能够实践这些插值的技巧,从而进一步掌握它们的应用。

019 插值 (1)

在这个练习中,我们讨论了如何在 JSX 中进行插值(Interpolation),特别是在 JSX 和 JavaScript 之间切换的方式。

关键点总结:

  1. JSX 与 JavaScript 的切换
    在 JSX 中,通过使用 {} 可以将 JavaScript 表达式嵌入到 JSX 中。当 JSX 编译时,任何在 {} 中的内容都会被当作 JavaScript 表达式进行评估,并插入到对应的位置。例如:

    const className = "container";
    const children = "Hello World";
    return <div className={className}>{children}</div>;

    这里的 classNamechildren 都是通过插值的方式传递给 JSX 元素。

  2. 插值的语法

    • 属性插值:可以将 JavaScript 表达式作为 JSX 元素的属性值,例如 className={className}
    • 内容插值:可以将 JavaScript 表达式作为元素的子元素插入,例如 <div>{children}</div>
  3. 切换状态

    • JSX 语法:当使用 <div></div> 等 JSX 语法时,React 正在解析类似于 HTML 的代码。
    • 进入 JavaScript 模式:当我们使用 {} 包裹表达式时,解析器会将其转换为 JavaScript 代码,执行后插入结果。
    • 回到 JSX 模式:一旦表达式结束,解析器会返回到 JSX 语法中。
  4. 自闭合标签
    在 JSX 中可以使用自闭合标签,例如 <img /><input />,这是 JSX 的一项简化功能,它允许你在没有子元素时不必书写结束标签。

  5. 表达式与逻辑

    • 只能插入表达式:在 JSX 中,你只能使用表达式,不能使用语句。例如,不能使用 iffor 语句,但可以使用三元运算符来控制逻辑:
      {isTrue ? <p>True</p> : <p>False</p>}
    • 表达式的编译:React 会将 JSX 中的插值内容直接编译成相应的 JavaScript 代码,将结果插入到最终的 DOM 中。

通过这些概念的掌握,你将能够更好地在 JSX 中处理动态内容和属性,使得你的 React 应用更加灵活和强大。

020 Spread props

在这个练习中,我们介绍了如何将一个包含多个属性的对象应用到 JSX 元素上,而无需单独为每个属性进行手动设置。你可能会经常遇到这种情况,尤其是在属性动态生成或者属性数量较多的场景下。

使用属性对象的传统方式:

假设我们有一个 props 对象,其中包含了 classNamechildren 等属性。常规的做法是这样写:

const props = {
  className: "container",
  children: "Hello World",
};

return (
  <div className={props.className}>
    {props.children}
  </div>
);

这需要你逐个指定对象中的属性,例如 className={props.className}children={props.children}

更优雅的方式——使用 JSX 的 Spread 属性:

为了避免手动传递每个属性,JSX 提供了一种更简洁的方法,即 属性扩展语法,类似于 JavaScript 中的 扩展操作符。你可以通过 ...props 将对象中的所有属性应用到 JSX 元素上:

const props = {
  className: "container",
  children: "Hello World",
};

return (
  <div {...props} />
);

工作原理:

...props 会将 props 对象中的每个键值对分别扩展为该元素的属性。上面的代码与以下手动传递属性的代码是等效的:

<div className="container">Hello World</div>

使用场景:

这种写法特别适合以下场景:

  1. 动态属性:当属性的数量和内容是动态变化时,例如根据 API 响应或函数返回的结果动态生成属性。
  2. 简化代码:减少重复代码,尤其是在属性较多的情况下,使代码更加简洁和易于维护。

通过使用这种方式,你可以更轻松地处理具有多个属性的 JSX 元素。

021 Spread props (1)

在这个练习中,我们探讨了如何在 JSX 中使用扩展语法将一个对象中的所有属性应用到某个元素上。这种方式对于处理动态属性或者复杂的属性集合时非常有用。

扩展操作符的工作原理

我们可以使用 ...props 将一个对象中的所有属性“展开”并应用到 JSX 元素上。例如:

const props = {
  className: "container",
  children: "Hello World",
};

return <div {...props} />;

这段代码会将 props 对象中的 classNamechildren 属性直接应用到 div 元素上,等效于手动指定这些属性:

<div className="container">Hello World</div>

覆盖属性

扩展操作符的一个关键点是属性覆盖。如果你在展开属性对象后又指定了同名的属性,那么 JSX 会采用后面定义的属性值。例如:

const props = {
  className: "container",
  children: "Hello World",
};

return <div {...props} className="my-container" />;

在这个例子中,className="my-container" 会覆盖 props 中的 className="container",因为它出现在 ...props 之后。

输出的结果是:

<div class="my-container">Hello World</div>

特殊的 children 属性

children 属性有点特殊。如果你直接在元素标签之间定义了内容,例如:

return <div {...props}>Goodbye World</div>;

此时,"Goodbye World" 会覆盖 props 对象中的 children 属性。换句话说,标签内部的内容优先级更高。

复杂的属性扩展

你可以将多个属性对象进行扩展,并且扩展顺序决定了最终应用的属性。例如:

const props1 = { className: "container" };
const props2 = { className: "my-container" };

return <div {...props1} {...props2} />;

在这个例子中,props2 中的 className="my-container" 会覆盖 props1 中的 className="container",因为 props2 位于 props1 之后。

输出的结果是:

<div class="my-container"></div>

通过这种方式,你可以灵活地管理和覆盖 JSX 元素的属性,同时减少手动编写重复代码的麻烦。

022 嵌套JSX

在这个练习中,我们将把之前使用 createElement 创建的 "Sam's Favorite Food" 列表(包含绿色鸡蛋和火腿)转化为使用 JSX 编写的版本。这将展示 JSX 在处理嵌套结构时的简洁性和可读性。

使用 JSX 进行嵌套

createElement 中,我们需要通过多层函数调用来构建嵌套的 HTML 结构。而使用 JSX 时,编写这样的嵌套结构会更加直观和接近原生的 HTML。以下是我们如何使用 JSX 来重现 "Sam's Favorite Food" 列表的代码:

function FavoriteFood() {
  return (
    <div className="container">
      <p>Sam's favorite food:</p>
      <ul className="sam-food">
        <li>Green Eggs</li>
        <li>Ham</li>
      </ul>
    </div>
  );
}

createElement 的区别

createElement 的多层函数调用不同,JSX 更加贴近 HTML 的语法,并且在可读性和直观性上有显著提高:

  1. 更接近 HTML: JSX 看起来就像 HTML,减少了函数嵌套的复杂性。
  2. 简洁明了: 你可以直接在代码中看到结构的嵌套层级,而不需要通过多个函数参数的方式进行传递。
  3. 代码更干净: 不需要反复调用 createElement,让代码变得更加简洁易懂。

额外注意点:JSX 与 HTML 的区别

  1. class 与 className: 在 JSX 中,HTML 中的 class 属性被改为 className,因为 class 是 JavaScript 中的保留字。
  2. 闭合标签: 在 JSX 中,像 <img /><br /> 这样的单标签元素需要自闭合。

通过这种方式,JSX 让我们更轻松地处理复杂的嵌套结构,尤其是在大型应用程序中,它可以极大地简化代码的编写与维护。

023 嵌套JSX (1)

在这个片段中,讲解了如何通过将 HTML 代码直接复制粘贴到 JSX 中,并且展示了在 JSX 中处理嵌套结构是多么简单和高效。然而,JSX 与 HTML 之间有一些重要的区别,尤其是关于属性的命名。例如:

  1. classclassName 的区别:在 JSX 中,我们不能像在 HTML 中那样使用 class 属性,因为 class 在 JavaScript 中是一个保留字。在 JSX 中,你需要使用 className 来指定 CSS 类名。这是因为 JSX 更关注的是 DOM 属性,而不是 HTML 属性。

  2. HTML 属性 vs. DOM 属性:JSX 使用的是 DOM 属性,而不是 HTML 属性。例如,表单中的 for 属性在 JSX 中应该写作 htmlFor。这种转换有助于避免与 JavaScript 保留字的冲突,并保持一致性。

  3. 改进后的编程体验:通过 JSX,我们不再需要使用 React.createElement 来手动创建和嵌套元素。JSX 更加直观和简洁,类似于 HTML,这使得编写复杂的用户界面变得更加容易。

结论:

使用 JSX 明显提高了代码的可读性和开发效率,特别是在处理复杂的嵌套结构时。虽然 JSX 和 HTML 之间存在一些细微的差异(如属性命名),但这都是为了与 JavaScript 保持一致,并优化开发体验。

024 片段

在这一段中,讲解了如何使用 React Fragments 来避免不必要的包裹元素,尤其是在某些布局需求(如 CSS Grid 或 Flexbox)中可能非常有用。

背景:

通常情况下,React 元素必须被一个单一的父元素包裹。这意味着如果你想返回多个兄弟元素(而不希望它们被一个 div 包裹),你可以使用 React Fragments 来实现。Fragments 允许你返回多个元素,而不在 DOM 中添加额外的包裹元素。

Fragments 有两种写法:

  1. 标准写法

    <React.Fragment>
      <Element1 />
      <Element2 />
    </React.Fragment>

    或者你可以先导入 Fragment

    import { Fragment } from 'react';

    然后:

    <Fragment>
      <Element1 />
      <Element2 />
    </Fragment>
  2. 简洁写法
    你可以用简写的形式,省略 Fragment 的名字,只需使用空标签包裹内容:

    <>
      <Element1 />
      <Element2 />
    </>

什么时候用 Fragment?

  • 布局需求:在使用诸如 CSS Grid 或 Flexbox 时,可能需要多个兄弟元素,但不希望它们被额外的容器元素包裹。
  • 返回多个兄弟元素:在 React 组件中返回多个元素,而不希望它们被包裹在 div 或其他容器标签中。

通过使用 Fragment,你可以确保只输出你需要的元素,而不会在 DOM 中添加额外的节点,这对于保持 DOM 结构简洁和避免不必要的布局影响非常有帮助。

总结:

通过使用 React Fragments,你可以避免多余的容器元素,同时保持 JSX 代码的简洁和清晰。这种技巧特别适合处理复杂的布局结构,或者在组件返回多个元素时更好地控制 DOM 结构。

025 片段 (1)

这一段解释了 React Fragments 的使用,并展示了为什么需要使用它来避免不必要的 div 包裹,同时还深入讲解了为何在 JavaScript 中无法直接返回多个顶层元素。

关键点:

  1. 问题的来源

    • 在 React 中,组件返回的 JSX 必须被一个父元素包裹。当你试图直接返回多个元素时,比如一个 div 和一个 ul,会导致编译错误,因为 JavaScript 无法让一个变量同时指向两个不同的值。
  2. 为什么不能直接移除父元素

    • 如果你去掉了包裹元素(比如 div),React 会抛出错误,因为它无法理解如何将多个兄弟元素直接渲染在 DOM 中。JavaScript 不支持给一个变量赋值多个值,这就是为什么必须有一个父元素包裹它们。
  3. React Fragments 的引入

    • 为了解决这个问题,React 提供了 Fragment,它是一个 "虚拟" 的容器,允许你返回多个元素,但不会在 DOM 中生成任何实际的包裹元素。
    • 这非常有用,尤其是在你想要避免多余的 div 或者 span,但仍然需要满足 React 的语法要求时。
  4. Fragment 的两种写法

    • 标准写法
      <React.Fragment>
        <Element1 />
        <Element2 />
      </React.Fragment>
    • 简写形式
      <>
        <Element1 />
        <Element2 />
      </>
    • 简写形式更简洁,通常开发者更喜欢使用。

结论:

通过使用 React Fragments,你可以在不生成多余 DOM 元素的情况下返回多个兄弟元素,这使得代码更简洁,同时也避免了多余的嵌套层级。它非常适合在 CSS 布局或组件返回多个元素时使用。

026 爸爸笑话时间 使用JSX

哈哈!这个笑话很轻松有趣:“什么样的贝果会飞?答案是——普通贝果(plane bagel)!” 这也是个非常好的提示,让我们放松一下、站起来活动活动。

现在正是时候伸展一下身体,去喝点水或者吃个小零食。如果有机会,还可以去对别人说点鼓励的话,帮助别人也会让自己感觉很好。之后回来继续学习更多有趣的 React 内容!

027 自定义组件简介

在这个练习中,我们开始了解自定义组件的概念。在 React 中,自定义组件其实很简单,它就是一个函数,该函数接受一个对象(通常是 props),并返回一些可以渲染的内容。那就是自定义组件的全部定义。

通常,返回的内容是 React 元素,但实际上它也可以是一个字符串、数字等可渲染的值。从本质上讲,自定义组件是你可以传递给 createElement API 的东西。在 JSX 中,自定义组件有专门的语法,可以让你轻松地在 JSX 中使用这些自定义组件。

这里有一个简单的例子:一个名为 greeting 的函数组件,它接收 props 对象,然后这些 props 就是你渲染该组件时传递的值。

在接下来的练习中,你会逐步通过这些步骤来熟悉自定义组件的创建和使用,学习 JSX 是如何将这些组件编译成实际的函数调用,并了解 React 是如何处理这些调用的。

这个过程很有趣,也非常有用。希望你在接下来的练习中玩得开心,祝你好运!

028 简单函数

在这个练习的第一部分,我们将逐步靠近创建一个可以生成可重用 JSX 的通用函数。现在我们有一个容器,它里面有两个消息,分别是“hello world”和“goodbye world”。显然,这里有一些重复的代码,我们可以优化它。

为了使代码更加通用,你可以编写一个函数,这个函数可以接收动态的子元素 children,从而减少重复。例如,你可以创建一个名为 message 的函数,它允许你传递不同的 children 内容。

这个练习的目标是让你实现这样一个通用的函数接口。虽然这还不是一个完整的 React 组件,但我们正在逐渐靠近 React 组件的形式。通过插值(interpolation),我们可以将函数调用的结果作为表达式插入 JSX 中,而函数返回的 React 元素可以作为 div 的子元素渲染。

任务:你需要编写这个通用的 message 函数,并通过插值将它的输出作为 JSX 的一部分。

祝你在这个过程中玩得愉快,完成之后我们会继续!

029 简单函数 (1)

在这个步骤中,我们通过创建一个 message 函数来减少代码重复。这个函数接收一个对象作为参数,并将其子元素 children 渲染到指定位置。最终,我们可以调用这个函数,传递需要显示的内容,从而避免代码的重复。

通过使用这个 message 函数,如果你想要在多个地方更新样式或结构,你只需要修改函数内部,而不必手动修改每个使用的地方。这就是减少代码重复的优势。

这个练习让我们更接近于 React 组件的结构,虽然当前的写法还不是完全的 React 组件形式,但它展示了如何通过参数化来灵活地处理 JSX 的渲染。在下一步中,我们将进一步探索真正的 React 组件并逐步优化这一过程。

祝你在这个过程愉快!

030 原生API

在这个步骤中,我们将进一步优化代码,使其更加符合 React 组件的语义,特别是通过使用 CreateElement API。

目标:

我们需要将自定义的 message 函数转化为真正的 React 组件,并通过 CreateElement API 来处理它,而不是直接调用函数。通过这种方式,React 将能够更好地管理组件的生命周期,比如何时渲染或更新组件。

步骤:

  1. 组件的定义:
    你仍然会保留 message 函数,但是不再直接调用它。相反,你会将这个函数作为一个特殊的元素传递给 React.createElement。这样 React 就会负责调用这个函数,而不是你手动调用它。

  2. 传递 props:
    在 React 中,当你通过 CreateElement API 创建一个元素时,所有的 props 都会被组合成一个对象并传递给组件。我们会把 children 作为这个 props 的一部分。

  3. Render 流程:
    通过 React.createElement API 传递 message 函数,然后由 React 来调用这个函数,并将 props 传递给它。

代码示例:

function Message({ children }) {
    console.log('Rendering Message component');
    return <div className="message">{children}</div>;
}

// 使用 CreateElement API
const element = React.createElement(Message, null, 'Hello World');

// Render 到 DOM
ReactDOM.createRoot(document.getElementById('root')).render(element);

在这个例子中,我们定义了一个 Message 组件,它接受一个 children prop 并渲染它。然后,我们使用 React.createElement 来创建这个组件的实例,而不是手动调用它。React 会自动处理组件的调用并传递 props。

通过这种方法,当你引入更多复杂的功能,比如 hooks 时,你会发现 React 组件的这种形式非常重要和方便。

你可以通过在 Message 函数和 React.createElement 的各个位置添加 console.log,来观察 React 是如何管理组件的渲染的。

总结:

  • 自定义 React 组件是函数,它们接收 props 并返回可渲染的内容。
  • React.createElement 是生成 React 元素的 API。
  • 使用 CreateElement API,React 将自动管理组件的渲染、更新等操作。

希望你在这个过程中愉快探索!

031 原生API (1)

在这个步骤中,我们通过使用 React.createElement 进一步加深了对 React 组件的理解,并学习了如何让 React 自己决定何时调用我们的自定义组件。通过这种方式,我们的组件更符合 React 的生命周期和渲染机制。

关键点总结:

  1. React.createElement 调用:
    我们将 message 函数作为自定义组件传递给 React.createElement,并让 React 负责在渲染时调用该组件。React 会传递 props,并在必要时调用组件函数。

  2. 延迟调用:
    使用 React.createElement 后,React 将延迟调用组件,直到真正需要渲染时才调用组件。这与我们直接调用组件函数的方式不同。之前的方式是立即调用函数,而现在是由 React 自己来控制。

  3. React 特有的元素类型:
    当我们使用 React.createElement 时,React 创建了一个特殊的 React 元素,其类型不是普通的 HTML 标签,而是我们的自定义组件。这为组件的扩展和复用提供了可能性。

  4. 生命周期:
    在我们继续深入理解 React 的状态管理和生命周期方法时,使用 React.createElement 这种方式的优势会变得更加明显。React 可以更高效地管理组件的渲染和更新,同时保持应用的状态隔离和一致性。

结论:

通过这一步,我们的组件已经在语义上符合了 React 的工作方式,即 React 自己负责组件的调用和管理。而接下来我们将进一步完善 JSX 语法,使自定义组件的使用更加简洁和直观。

希望这个过程帮助你更好地理解了 React.createElement 的机制和自定义组件的工作原理。

032 JSX组件

在这个练习中,我们的目标是从使用 React.createElement 过渡到更简洁、易读的 JSX 语法。不过需要注意的是:自定义组件的名称大小写非常重要。

关键概念:JSX 中的大小写规则

在使用 JSX 时,React 会将小写字母开头的名称(例如 divspan)视为 HTML 标签。如果你使用了小写的自定义组件名称(例如 message),React 会将它当作一个字符串,认为它是一个 HTML 标签。然而,当你将名称首字母大写(例如 Message)时,React 就知道这是一个函数引用(自定义组件)。

操作步骤:

  1. 首先不要大写自定义组件的名称,将它切换为 JSX 语法,并观察 React 在编译后的输出是如何处理的。
  2. 然后将函数名称首字母大写,看看 React 如何正确识别它为一个自定义组件。

通过这个过程,你将理解 React 如何编译 JSX 以及它如何区分 HTML 元素和自定义组件。

步骤:

  1. 不大写组件名称:
    首先,将你的自定义组件名称写成小写形式,并查看 React 如何将其编译为字符串:

    const element = (
      <div>
        <message>Hello World</message>
      </div>
    );
  2. 将名称首字母大写:
    现在,将组件名称首字母大写,查看编译后的不同:

    const element = (
      <div>
        <Message>Hello World</Message>
      </div>
    );

结果:

当你将名称大写时,React 会识别 Message 是一个自定义组件,而不是一个 HTML 标签,并会调用函数,而不是将其当作字符串标签名处理。

这是以后你在 JSX 中编写 React 组件的标准方式,它比 React.createElement 提供了更清晰、可读性更高的语法。

033 JSX组件 (1)

一开始,我们会将 React.createElement 替换为 JSX 语法。让我们把 message 标签应用到 JSX 中:

<Message>Hello World</Message>
<Message>Goodbye World</Message>

发现问题

表面上看,这似乎是有效的,但我们会得到一个警告:“浏览器无法识别标签 message”。这意味着 JSX 正在尝试将 message 作为一个 HTML 标签,而不是 React 组件。React 提示我们:如果要渲染一个 React 组件,请将名称的首字母大写。

解决问题

为了让 React 识别这是一个自定义组件,而不是原生 DOM 元素,我们需要将自定义组件的名称首字母大写:

<Message>Hello World</Message>
<Message>Goodbye World</Message>

解析过程

当我们查看编译后的输出时,原来使用小写 message 时,React 编译器会将其解释为字符串 'message',即浏览器尝试渲染一个不存在的原生 DOM 元素。而当我们将其大写为 Message 后,React 就能识别这是一个函数引用,并调用对应的 React 组件。

通过这种方式,你可以轻松地创建并使用自定义组件,并遵循 React 的约定使代码更加规范和清晰。

小结

React 使用组件名称的大小写来区分 HTML 元素和自定义组件。因此,当定义自定义组件时,始终记得将其名称首字母大写,以确保 JSX 能够正确编译并渲染组件。

034 Props

这个练习是为了加深你对自定义组件和 props 的理解。我们将创建一个简单的计算器组件,这个组件将接收 leftoperatorright 作为属性,并渲染一个包含计算结果的 div

任务概述

你需要实现一个 Calculator 组件,它会:

  1. 接受 left(左操作数)、operator(运算符)和 right(右操作数)作为 props
  2. 根据这些 props 生成一个计算表达式,比如:3 + 5
  3. 然后输出这个表达式的结果。

示例代码

下面是一个简单的实现思路:

function Calculator({ left, operator, right }) {
  let result;

  switch (operator) {
    case '+':
      result = left + right;
      break;
    case '-':
      result = left - right;
      break;
    case '*':
      result = left * right;
      break;
    case '/':
      result = right !== 0 ? left / right : 'Error';
      break;
    default:
      result = 'Invalid operator';
  }

  return (
    <div>
      {left} {operator} {right} = {result}
    </div>
  );
}

// 使用示例
<Calculator left={3} operator="+" right={5} />
<Calculator left={10} operator="*" right={2} />

解析

  • Calculator 组件使用 props 来接收 leftoperatorright
  • 根据 operator 的值,组件会执行相应的数学运算。
  • 最终将 leftoperatorright 和结果渲染到页面上。

下一步

尝试使用不同的 props 调用 Calculator 组件,并查看其工作效果。这个练习旨在帮助你理解如何通过 props 在 React 中传递数据并创建动态组件。

035 Props (1)

在这个练习中,我们创建了一个 Calculator 组件,并通过解构 props 来获取 leftoperatorright 三个参数。然后根据这些参数执行运算,并展示结果。

解析

  1. 解构 props
    我们从传入的 props 对象中解构出 leftoperatorright。这种方式可以让代码更加简洁和可读。

  2. 执行运算:
    使用 operator 来决定执行哪种运算(加、减、乘、除),并将结果存储在 result 变量中。这个步骤模拟了简单的数学运算。

  3. 渲染结果:
    我们使用 JSX 来将 leftoperatorright 的值显示出来,并将计算结果显示在 output 标签中,以便于提高无障碍性。

示例代码:

function Calculator({ left, operator, right }) {
  let result;

  switch (operator) {
    case '+':
      result = left + right;
      break;
    case '-':
      result = left - right;
      break;
    case '*':
      result = left * right;
      break;
    case '/':
      result = right !== 0 ? left / right : 'Error';
      break;
    default:
      result = 'Invalid operator';
  }

  return (
    <div>
      <code>{left} {operator} {right}</code> = <output>{result}</output>
    </div>
  );
}

// 使用示例
<Calculator left={1} operator="+" right={2} />
<Calculator left={1} operator="/" right={2} />

关键点

  1. 灵活的 props
    组件的 props 可以是任意类型,不仅限于原始类型(如数字、字符串),还可以是对象或其他 React 元素。

  2. JSX 语法:
    在 JSX 中,通过 {} 可以插入 JavaScript 表达式来显示变量或计算结果。

  3. 组件本质:
    React 组件本质上是一个函数,它接受 props 作为参数,并返回需要渲染的 JSX。

通过这个练习,你可以进一步熟悉如何使用 props 传递数据,并在 React 组件中灵活渲染动态内容。

036 爸爸笑话时间 自定义组件

哈哈,Michael Jackson 的笑话和 React Router 的双关真是有趣!看来你学到很多 React 的知识,确实是时候让这些内容在大脑中“炖一炖”了!趁着这个休息时间,站起来活动一下,让身体和大脑都焕然一新。

在你休息之后,我们还会继续深入学习 React 的精彩部分。我很期待你回来,咱们继续一起学习 React 的其他酷炫内容!

037 TypeScript简介

从现在开始,我们将使用 TypeScript 来完成所有的开发。TypeScript 是一种建立在 JavaScript 之上的强类型语言,它可以为你带来类型安全性。

为什么要用 TypeScript?

类型安全的最大好处就是,当别人调用你的函数时,如果传递的参数类型不对,TypeScript 会在编译时给你报错。比如,如果一个函数需要传入字符串(因为可能会用到 .toUpperCase() 这样的操作),但你传入了一个数字,TypeScript 就会提前告知你。在普通的 JavaScript 中,这是不会被捕获的。

整个行业目前基本都在使用 TypeScript,你也肯定想跟上这个趋势。接下来我们将开始在这个练习中探索 TypeScript。

为什么选择 TypeScript?

有些人可能会觉得 TypeScript 会给他们的代码带来额外的“红色波浪线”,但其实这些都是帮助你避免潜在错误的警告。可以把 TypeScript 想象成一个“无情但诚实的朋友”,虽然它会指出代码中的问题,但它的目的是为了避免你犯下严重的错误。

TypeScript 和 React 的结合

React 组件本质上是接受对象(props)的函数,并返回可以渲染的东西。从 TypeScript 的角度来看,你只需要为这些函数定义类型。没有什么特别的规则,就是为函数加上类型定义。

TypeScript 函数的类型定义

这是一个普通的 JavaScript 函数:

function getUserDisplayName(user) {
  return user.name || 'Unknown';
}

如果我们用 TypeScript,可以这样定义类型:

function getUserDisplayName(user: { name?: string }): string {
  return user.name || 'Unknown';
}

上面的 user 参数必须是一个对象,并且可能有一个可选的 name 属性,类型为 string。TypeScript 会帮你确保返回的结果始终是一个 string

TypeScript 中的 React 组件

下面是一个没有类型定义的 React 组件:

const Message = ({ children }) => <div>{children}</div>;

我们可以使用 TypeScript 为 children 添加类型:

const Message: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  return <div>{children}</div>;
};

这样,children 可以是任何 React 可渲染的内容,比如字符串、元素或其它组件。

使用 TypeScript 的技巧

如果你刚开始使用 TypeScript,可能会因为某些类型错误感到困惑。这时候,不妨使用 // @ts-expect-error 来临时忽略这些错误。等你对 TypeScript 更加熟悉后,可以回过头来修复这些问题。

我们将在接下来的练习中更深入地使用 TypeScript,帮助你写出更加健壮的 React 代码。

038 Props (2)

在这个练习中,我们将为计算器组件添加类型安全,以确保左操作数、运算符和右操作数都具有正确的类型。这将帮助我们避免潜在的错误,比如传入一个字符串而不是数字。

步骤一:定义 CalculatorProps 类型

首先,我们需要定义一个类型来表示计算器的属性(props)。这可以通过创建一个接口来完成:

interface CalculatorProps {
  left: number;       // 左操作数
  operator: '+' | '-' | '*' | '/'; // 运算符
  right: number;      // 右操作数
}

步骤二:更新计算器组件

接下来,我们将使用这个类型来更新计算器组件的 props:

const Calculator: React.FC<CalculatorProps> = ({ left, operator, right }) => {
  let result: number;

  switch (operator) {
    case '+':
      result = left + right;
      break;
    case '-':
      result = left - right;
      break;
    case '*':
      result = left * right;
      break;
    case '/':
      result = right !== 0 ? left / right : 0; // 防止除以零
      break;
    default:
      throw new Error(`Unknown operator: ${operator}`);
  }

  return (
    <div>
      <code>
        {left} {operator} {right} = {result}
      </code>
    </div>
  );
};

步骤三:在应用中使用计算器组件

最后,我们可以在应用中使用这个组件,并确保传递正确的 props:

<Calculator left={1} operator="+" right={2} />
<Calculator left={5} operator="-" right={3} />
<Calculator left={4} operator="*" right={2} />
<Calculator left={10} operator="/" right={2} />

注意事项

  • 如果你尝试传递错误的类型,比如将 left 设置为一个字符串,TypeScript 将会抛出错误,提醒你这个 prop 的类型不匹配。
  • 对于 operator,我们使用了字符串字面量类型来限制允许的运算符,以便获得自动完成功能。

通过这样的设置,我们的计算器组件现在就具备了类型安全,这不仅提高了代码的健壮性,也提高了开发过程中的便利性。您可以继续在应用中添加更多的计算器实例,看看 TypeScript 如何为您提供更好的开发体验。

039 Props (3)

在这个练习中,我们将通过添加类型安全来改进计算器组件的使用体验。这样,开发者在使用组件时可以获得更好的错误提示和自动补全功能。

步骤一:定义 CalculatorProps 类型

首先,我们需要定义一个类型 CalculatorProps,以确保传递给计算器组件的参数符合预期:

interface CalculatorProps {
  left: number; // 左操作数
  operator: '+' | '-' | '*' | '/'; // 运算符,可以限制为特定的字符串
  right: number; // 右操作数
}

步骤二:更新计算器组件

接下来,我们将使用这个类型更新计算器组件的 props:

const Calculator: React.FC<CalculatorProps> = ({ left, operator, right }) => {
  let result: number;

  switch (operator) {
    case '+':
      result = left + right;
      break;
    case '-':
      result = left - right;
      break;
    case '*':
      result = left * right;
      break;
    case '/':
      result = right !== 0 ? left / right : 0; // 防止除以零
      break;
    default:
      throw new Error(`Unknown operator: ${operator}`);
  }

  return (
    <div>
      <code>
        {left} {operator} {right} = {result}
      </code>
    </div>
  );
};

步骤三:在应用中使用计算器组件

使用组件时,我们确保传递正确的类型。例如:

<Calculator left={1} operator="+" right={2} />
<Calculator left={5} operator="-" right={3} />
<Calculator left={4} operator="*" right={2} />
<Calculator left={10} operator="/" right={2} />

错误测试

为了确保类型安全,我们可以故意传递错误的类型,比如将 left 设置为字符串,TypeScript 将会抛出错误,提醒你这个 prop 的类型不匹配:

<Calculator left={"one"} operator="+" right={2} /> // 会产生错误

小结

通过这种方式,我们可以确保组件的 props 类型安全,同时在开发过程中获得更好的错误提示和自动补全功能。这样做不仅提高了代码的可维护性,也提升了开发者的体验。继续在应用中添加更多的计算器实例,测试 TypeScript 的类型检查功能,确保你熟悉它的用法。

040 类型收窄

要将 operator 的类型限制为特定的字符串(如“+”、“-”、“*”和“/”),我们可以在 CalculatorProps 接口中使用字符串字面量类型。这将使 TypeScript 在编译时检查 operator 的值是否符合这些特定的运算符。

步骤一:更新 CalculatorProps 类型

首先,我们需要更新 CalculatorProps 接口,具体如下:

interface CalculatorProps {
  left: number; // 左操作数
  operator: '+' | '-' | '*' | '/'; // 限制运算符为特定的字符串
  right: number; // 右操作数
}

步骤二:更新计算器组件

在更新了类型定义后,我们的计算器组件会自动获得这些类型的安全性。代码如下:

const Calculator: React.FC<CalculatorProps> = ({ left, operator, right }) => {
  let result: number;

  switch (operator) {
    case '+':
      result = left + right;
      break;
    case '-':
      result = left - right;
      break;
    case '*':
      result = left * right;
      break;
    case '/':
      result = right !== 0 ? left / right : 0; // 防止除以零
      break;
    default:
      throw new Error(`Unknown operator: ${operator}`);
  }

  return (
    <div>
      <code>
        {left} {operator} {right} = {result}
      </code>
    </div>
  );
};

步骤三:测试类型安全

现在,当你尝试将不支持的运算符传递给 Calculator 组件时,TypeScript 会在编译时发出错误:

<Calculator left={1} operator="+" right={2} /> // 正确
<Calculator left={5} operator="-" right={3} /> // 正确
<Calculator left={4} operator="*" right={2} /> // 正确
<Calculator left={10} operator="/" right={2} /> // 正确
<Calculator left={10} operator="%" right={2} /> // 错误:类型“%”不可赋值给类型“'+' | '-' | '*' | '/'”。

小结

通过将运算符类型限制为字符串字面量类型,开发者可以在编译时捕获潜在的错误,而不必在运行时检查。这提高了代码的安全性和可维护性。继续使用这些特性,在你的应用程序中进一步应用 TypeScript,确保在构建更复杂的功能时保持类型安全。

041 类型收窄 (1)

要限制运算符为特定字符串(如加法、减法、乘法和除法),并获得更好的类型安全和自动完成功能,可以使用 TypeScript 的字符串字面量类型。你已经成功实现了这个过程,并且得到了 TypeScript 的类型检查和代码编辑器的自动完成功能。这是 TypeScript 的强大之处,能够在开发过程中提供实时的反馈。

解决方案回顾

以下是你所做的关键步骤的总结:

  1. 定义 CalculatorProps 接口

    interface CalculatorProps {
      left: number; 
      operator: '+' | '-' | '*' | '/'; // 使用字符串字面量类型限制运算符
      right: number; 
    }
  2. 在组件中使用这些类型
    Calculator 组件中,你使用这些定义的类型来确保正确的参数传递。

    const Calculator: React.FC<CalculatorProps> = ({ left, operator, right }) => {
        // 计算逻辑...
    };
  3. 错误处理
    通过 TypeScript 的类型检查,当你尝试传递不允许的运算符(如 **^)时,编辑器将给出错误提示,而不必运行应用程序。

进一步的改进

虽然当前实现已经很好地解决了问题,但你提到可以进一步优化。以下是一些潜在的改进方向:

  1. 使用枚举
    如果运算符数量较多或可能变化,考虑使用枚举来管理运算符。

    enum Operator {
      Add = '+',
      Subtract = '-',
      Multiply = '*',
      Divide = '/',
    }
    
    interface CalculatorProps {
      left: number;
      operator: Operator; // 使用枚举类型
      right: number;
    }
  2. 自动化运算符提示
    结合 TypeScript 的类型提示功能,增强用户体验。例如,可以在函数内部提供详细的错误信息或提示,指导用户如何正确使用组件。

  3. 运行时检查
    尽管 TypeScript 提供了静态检查,但在组件运行时也可以加入检查,以确保运算符的有效性(例如,抛出错误或警告)。

通过这些方法,你不仅可以提高代码的可读性和可维护性,还能让使用组件的开发者获得更好的体验。继续探索 TypeScript 的特性,将其应用于你的 React 项目中,以构建更安全、更高效的应用程序!

042 推导类型

为了使 TypeScript 的运算符更具可扩展性并避免手动添加每个运算符,你可以使用类似于之前所述的typeofkeyof关键字来创建派生类型。这样,我们就可以从现有的运算符对象中提取出所有的运算符,而不是手动维护一个字符串字面量类型。

实现步骤

以下是你可以遵循的步骤,以便在 TypeScript 中实现更灵活的运算符类型:

  1. 定义一个运算符对象
    首先,定义一个包含所有有效运算符的对象。这将允许你从这个对象中提取出运算符。

    const operations = {
      add: '+',
      subtract: '-',
      multiply: '*',
      divide: '/',
    } as const; // 使用 `as const` 来确保这是一个只读对象
  2. 提取运算符类型
    然后,使用typeofkeyof来提取运算符的类型。

    type Operator = typeof operations[keyof typeof operations]; // 这将是 '+' | '-' | '*' | '/'
  3. 更新 CalculatorProps 接口
    修改 CalculatorProps 接口,使其使用新的运算符类型。

    interface CalculatorProps {
      left: number;
      operator: Operator; // 使用派生的 Operator 类型
      right: number;
    }

完整示例

以下是一个完整的示例代码,展示了如何实现上述步骤:

const operations = {
  add: '+',
  subtract: '-',
  multiply: '*',
  divide: '/',
} as const;

type Operator = typeof operations[keyof typeof operations]; // '+' | '-' | '*' | '/'

interface CalculatorProps {
  left: number;
  operator: Operator;
  right: number;
}

const Calculator: React.FC<CalculatorProps> = ({ left, operator, right }) => {
  let result: number;

  switch (operator) {
    case '+':
      result = left + right;
      break;
    case '-':
      result = left - right;
      break;
    case '*':
      result = left * right;
      break;
    case '/':
      result = left / right;
      break;
    default:
      throw new Error(`Unsupported operator: ${operator}`);
  }

  return (
    <div>
      <code>
        {left} {operator} {right} = {result}
      </code>
    </div>
  );
};

总结

通过这种方式,你可以方便地添加或删除运算符,而不必手动维护类型定义。当你更新运算符对象时,TypeScript 会自动反映这些更改,保持类型的一致性。这种方法在开发过程中可以减少错误并提高代码的可维护性。

043 推导类型 (1)

很高兴你已经成功实现了运算符的类型派生!下面是对这个解决方案的进一步分析,帮助你理解这一过程的细节和优势。

概要

你所使用的解决方案利用了 TypeScript 的类型系统,通过从一个包含有效运算符的对象中提取出运算符类型。这不仅简化了代码,还使得类型定义更加灵活和可扩展。

具体实现

  1. 定义运算符对象
    你创建了一个对象 operations,它包含了所有有效的运算符,并且使用 as const 确保它是一个只读的常量。这使得 TypeScript 能够推断出对象的字面量类型。

    const operations = {
      add: '+',
      subtract: '-',
      multiply: '*',
      divide: '/',
      // 可以在这里添加其他运算符
    } as const;
  2. 提取运算符类型
    你通过使用 typeofkeyof 创建了一个运算符类型,这个类型自动从 operations 对象中派生而来。

    type Operator = typeof operations[keyof typeof operations]; // 结果是 '+' | '-' | '*' | '/'
  3. 更新 CalculatorProps 接口
    最后,你更新了 CalculatorProps 接口,将 operator 的类型改为新的 Operator 类型。

    interface CalculatorProps {
      left: number;
      operator: Operator; // 使用派生的 Operator 类型
      right: number;
    }

优势

  • 自动推断:通过这种方式,任何时候你更新 operations 对象,都能自动反映在 Operator 类型中,避免了手动同步的麻烦。

  • 类型安全:TypeScript 会确保在使用组件时,只有有效的运算符可以被传递,这大大减少了运行时错误的风险。

  • 开发体验:使用 TypeScript 提供的自动完成功能,使得编写代码时更加高效和愉快。

总结

这种类型派生的方式非常有效,能够让你的代码更易于维护和扩展。继续保持这种方式,不断优化你的代码!如果有其他问题或需要进一步的帮助,请随时问我。

044 默认Props

好的,我们来讨论如何为计算器组件设置默认值并使某些属性变为可选属性。在 TypeScript 中,我们可以通过定义接口来实现这一点,同时结合提供默认值的方式。下面是如何完成这一任务的步骤。

步骤概述

  1. 修改 CalculatorProps 接口:使 leftrightoperator 属性变为可选。
  2. 设置默认值:在组件内部为这些属性提供默认值。

具体实现

1. 修改 CalculatorProps 接口

首先,我们需要更新 CalculatorProps 接口,以便将 leftrightoperator 属性标记为可选的。使用问号 ? 表示属性是可选的。

interface CalculatorProps {
    left?: number;      // 可选属性
    operator?: Operator; // 可选属性
    right?: number;     // 可选属性
}

2. 设置默认值

在你的计算器组件内部,你可以使用逻辑运算符来设置默认值。这样,如果传入的 props 中没有这些属性,组件就会使用默认值。

const Calculator: React.FC<CalculatorProps> = ({ left = 0, operator = '+', right = 0 }) => {
    // 现在 left、operator 和 right 都有默认值
    const result = operations[operator](left, right); // 计算结果
    return (
        <div>
            <code>{`${left} ${operator} ${right}`}</code>
            = <output>{result}</output>
        </div>
    );
};

完整代码示例

下面是完整的示例代码,展示如何将所有这些组合在一起:

import React from 'react';

// 定义运算符
const operations = {
    add: (a: number, b: number) => a + b,
    subtract: (a: number, b: number) => a - b,
    multiply: (a: number, b: number) => a * b,
    divide: (a: number, b: number) => a / b,
} as const;

// 定义可选的运算符类型
type Operator = typeof operations[keyof typeof operations];

// 定义 CalculatorProps 接口
interface CalculatorProps {
    left?: number;
    operator?: Operator;
    right?: number;
}

// Calculator 组件
const Calculator: React.FC<CalculatorProps> = ({ left = 0, operator = 'add', right = 0 }) => {
    const result = operations[operator](left, right);
    return (
        <div>
            <code>{`${left} ${operator} ${right}`}</code>
            = <output>{result}</output>
        </div>
    );
};

// 使用 Calculator 组件
const App = () => (
    <div>
        <Calculator left={1} right={2} operator="add" />
        <Calculator right={5} operator="multiply" />
        <Calculator left={4} />
        <Calculator />
    </div>
);

export default App;

总结

通过这些步骤,你已经成功地为计算器组件实现了可选属性和默认值。这不仅提高了组件的灵活性,也使得使用组件时的开发体验更加友好。如果用户不传入某些属性,组件会自动使用预设的默认值。这样,你可以确保组件始终能正常工作。继续加油!如果你还有其他问题,欢迎随时问我。

045 默认Props (1)

好的,我们来总结一下如何在 TypeScript 中为 React 组件设置可选属性和默认值。在这段代码中,我们的目标是为计算器组件的 leftrightoperator 属性提供默认值,并确保这些属性是可选的。

实现步骤

  1. 修改属性接口:将 leftrightoperator 属性标记为可选属性。
  2. 设置默认值:在组件内部为这些属性提供默认值。

具体实现

以下是一个简单的实现示例:

import React from 'react';

// 定义运算符类型
type Operator = 'add' | 'subtract' | 'multiply' | 'divide';

// 定义计算器属性接口
interface CalculatorProps {
    left?: number;      // 可选属性
    operator?: Operator; // 可选属性
    right?: number;     // 可选属性
}

// 定义运算操作
const operations: Record<Operator, (a: number, b: number) => number> = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b,
    multiply: (a, b) => a * b,
    divide: (a, b) => a / b,
};

// Calculator 组件
const Calculator: React.FC<CalculatorProps> = ({ left = 0, operator = 'add', right = 0 }) => {
    // 计算结果
    const result = operations[operator](left, right);
    
    return (
        <div>
            <code>{`${left} ${operator} ${right}`}</code>
            = <output>{result}</output>
        </div>
    );
};

// 使用 Calculator 组件
const App = () => (
    <div>
        <Calculator left={1} right={2} operator="add" />
        <Calculator right={5} operator="multiply" />
        <Calculator left={4} />
        <Calculator />
    </div>
);

export default App;

代码说明

  1. 接口定义:我们定义了 CalculatorProps 接口,其中 leftrightoperator 都是可选属性。

    interface CalculatorProps {
        left?: number;
        operator?: Operator;
        right?: number;
    }
  2. 默认值设置:在组件定义中,我们为 leftoperatorright 属性设置了默认值。这意味着如果没有传入这些属性,组件会使用默认值:

    const Calculator: React.FC<CalculatorProps> = ({ left = 0, operator = 'add', right = 0 }) => {
        const result = operations[operator](left, right);
        // ... 组件的返回部分
    };
  3. 运算操作:使用一个记录类型 Record<Operator, (a: number, b: number) => number> 来定义运算操作,这样我们就可以安全地引用运算符,确保不会传入错误的运算符。

测试默认值和可选属性

  • 当我们使用 Calculator 组件而不传入某个属性时,比如不传入 left,则会使用默认值 0
  • 如果我们传入了一个不被支持的运算符,TypeScript 会提示错误。

总结

通过这次的实现,你已经成功为计算器组件设置了可选属性和默认值。这不仅提高了组件的灵活性,也使得使用组件时的开发体验更加友好。继续加油,如果你有其他问题,随时可以问我!

046 减少重复

在这一步中,我们将专注于优化 TypeScript 的类型定义,以便更好地处理数学运算,而不需要在每个操作中重复定义函数的类型。这将使我们能够轻松添加新的操作而不必担心类型声明。

目标

我们希望创建一个操作类型,以便我们可以从中导出所需的参数类型和返回类型,而无需手动指定每个操作的详细类型。

实现步骤

  1. 定义一个通用的操作类型:我们可以定义一个类型 Operation,它是一个接受两个数字参数并返回数字的函数类型。
  2. 创建一个操作对象:在这个对象中,我们将每个运算符作为属性,并将对应的运算函数赋值给它。
  3. 使用类型推断:利用 TypeScript 的类型推断功能来自动推断参数和返回值的类型。

示例实现

下面是一个简化的实现示例:

import React from 'react';

// 定义运算符类型
type Operator = 'add' | 'subtract' | 'multiply' | 'divide';

// 定义操作类型
type Operation = (left: number, right: number) => number;

// 创建操作对象
const operations: Record<Operator, Operation> = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b,
    multiply: (a, b) => a * b,
    divide: (a, b) => a / b,
};

// 定义计算器属性接口
interface CalculatorProps {
    left?: number;
    operator?: Operator;
    right?: number;
}

// 计算器组件
const Calculator: React.FC<CalculatorProps> = ({
    left = 0,
    operator = 'add',
    right = 0,
}) => {
    const result = operations[operator](left, right);

    return (
        <div>
            <code>{`${left} ${operator} ${right}`}</code>
            = <output>{result}</output>
        </div>
    );
};

// 使用 Calculator 组件
const App = () => (
    <div>
        <Calculator left={1} right={2} operator="add" />
        <Calculator right={5} operator="multiply" />
        <Calculator left={4} />
        <Calculator />
    </div>
);

export default App;

代码解释

  1. 定义 Operation 类型:我们定义了一个 Operation 类型,表示接受两个 number 类型参数并返回 number 的函数。

    type Operation = (left: number, right: number) => number;
  2. 创建 operations 对象:使用 Record<Operator, Operation> 来定义一个运算对象,允许我们将每个运算符与其对应的实现关联起来。

    const operations: Record<Operator, Operation> = {
        add: (a, b) => a + b,
        subtract: (a, b) => a - b,
        multiply: (a, b) => a * b,
        divide: (a, b) => a / b,
    };
  3. 计算器组件:在 Calculator 组件中,我们使用 operations 对象来执行所需的运算,而不需要为每个操作手动定义类型。

优点

  • 简化代码:通过使用统一的 Operation 类型,我们减少了冗余的类型声明。
  • 易于扩展:当我们需要添加新的运算时,只需在 operations 对象中添加新项即可,无需更改类型定义。

结论

通过以上步骤,我们成功地优化了 TypeScript 的类型定义,使得添加新操作变得更加容易。这样的结构不仅提高了代码的可读性和可维护性,也使得开发过程更加顺畅。如果你有其他问题或需要进一步的帮助,请随时告诉我!

047 减少重复 (1)

在这个阶段,我们要进一步改进我们的 TypeScript 类型定义,以确保我们的计算器操作具备更好的类型安全和自动完成功能。我们需要处理的主要任务是定义一个 Operation 函数类型,并利用这个类型来确保我们能够限制操作符的可用性,同时又不会牺牲类型安全性。

目标

  1. 定义操作类型:创建一个 Operation 函数类型,该类型接受两个数字参数并返回一个数字。
  2. 限制操作符:使用 TypeScript 的联合类型来限制可用的操作符。
  3. 确保类型安全性:在定义运算时确保 TypeScript 知道它们的类型,从而能够提供正确的类型检查和自动完成功能。

示例实现

以下是一个改进的实现示例:

import React from 'react';

// 定义运算符类型
type Operator = 'add' | 'subtract' | 'multiply' | 'divide';

// 定义运算类型
type Operation = (left: number, right: number) => number;

// 创建操作对象,使用 Record 来限定操作符
const operations: Record<Operator, Operation> = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b,
    multiply: (a, b) => a * b,
    divide: (a, b) => a / b,
};

// 定义计算器属性接口
interface CalculatorProps {
    left?: number;
    operator?: Operator;
    right?: number;
}

// 计算器组件
const Calculator: React.FC<CalculatorProps> = ({
    left = 0,
    operator = 'add',
    right = 0,
}) => {
    const result = operations[operator](left, right);

    return (
        <div>
            <code>{`${left} ${operator} ${right}`}</code>
            = <output>{result}</output>
        </div>
    );
};

// 使用 Calculator 组件
const App = () => (
    <div>
        <Calculator left={1} right={2} operator="add" />
        <Calculator right={5} operator="multiply" />
        <Calculator left={4} />
        <Calculator />
    </div>
);

export default App;

代码解释

  1. 定义操作符类型:我们定义了一个 Operator 类型,它只允许 'add', 'subtract', 'multiply', 和 'divide' 这四个字符串。

    type Operator = 'add' | 'subtract' | 'multiply' | 'divide';
  2. 定义运算类型:定义了一个 Operation 类型,用于表示运算函数。

    type Operation = (left: number, right: number) => number;
  3. 创建操作对象:使用 Record<Operator, Operation> 来确保 operations 对象中的每个运算符都与相应的操作函数匹配。

    const operations: Record<Operator, Operation> = {
        add: (a, b) => a + b,
        subtract: (a, b) => a - b,
        multiply: (a, b) => a * b,
        divide: (a, b) => a / b,
    };
  4. 定义计算器组件:在 Calculator 组件中,定义了 CalculatorProps 接口,以确保左侧和右侧参数为可选的数字,操作符为可选的 Operator 类型。

关键点

  • 自动完成与类型检查:通过将操作符类型限制为一个小的字符串集,我们能够利用 TypeScript 的自动完成特性,确保用户在传递参数时仅能选择有效的操作符。

  • 可选参数与默认值:我们为左侧和右侧数字以及操作符提供了默认值,使得使用计算器组件时更加灵活和友好。

结论

通过以上步骤,我们成功实现了对计算器组件的类型安全检查和操作符限制,使得代码更加健壮。这样的结构不仅提高了代码的可读性和可维护性,也使得开发过程更加顺畅。如果你还有其他问题或者需要进一步的帮助,请随时告诉我!

048 Satisfies

在这个步骤中,我们将通过使用 TypeScript 的 satisfies 关键字来改进我们的 operations 对象的类型定义。这样做可以简化类型声明,同时保持类型安全和良好的开发体验。

目标

  1. 使用 satisfies 语法:我们将使 operations 对象满足更广泛的类型定义。
  2. 避免重复的类型定义:通过使 operations 符合更广泛的类型,而不是强制性地将其定义为一个特定的类型。

示例实现

以下是如何实现的示例代码:

import React from 'react';

// 定义运算符类型
type Operator = 'add' | 'subtract' | 'multiply' | 'divide';

// 定义运算类型
type Operation = (left: number, right: number) => number;

// 创建操作对象
const operations = {
    add: (a: number, b: number) => a + b,
    subtract: (a: number, b: number) => a - b,
    multiply: (a: number, b: number) => a * b,
    divide: (a: number, b: number) => a / b,
} satisfies Record<Operator, Operation>; // 使用 satisfies 语法

// 定义计算器属性接口
interface CalculatorProps {
    left?: number;
    operator?: Operator;
    right?: number;
}

// 计算器组件
const Calculator: React.FC<CalculatorProps> = ({
    left = 0,
    operator = 'add',
    right = 0,
}) => {
    const result = operations[operator](left, right);

    return (
        <div>
            <code>{`${left} ${operator} ${right}`}</code>
            = <output>{result}</output>
        </div>
    );
};

// 使用 Calculator 组件
const App = () => (
    <div>
        <Calculator left={1} right={2} operator="add" />
        <Calculator right={5} operator="multiply" />
        <Calculator left={4} />
        <Calculator />
    </div>
);

export default App;

代码解释

  1. 运算符类型:我们定义了一个 Operator 类型,它限制了可用的运算符。

    type Operator = 'add' | 'subtract' | 'multiply' | 'divide';
  2. 运算类型:定义了一个 Operation 类型,表示接收两个数字并返回一个数字的函数。

    type Operation = (left: number, right: number) => number;
  3. 使用 satisfies 语法:在 operations 对象中,使用 satisfies 关键字来指定 operations 应符合 Record<Operator, Operation> 类型,而不强制为特定的类型。这使得我们可以更灵活地扩展 operations 对象而不会破坏类型检查。

    const operations = {
        add: (a: number, b: number) => a + b,
        subtract: (a: number, b: number) => a - b,
        multiply: (a: number, b: number) => a * b,
        divide: (a: number, b: number) => a / b,
    } satisfies Record<Operator, Operation>;

关键点

  • 简化类型定义:通过使用 satisfies 关键字,我们能够减少冗余的类型定义,使代码更加简洁易读。
  • 保留类型安全:此方法允许 TypeScript 保留对 operations 的类型检查,并确保我们只使用有效的操作符。

结论

通过这个步骤,我们不仅优化了类型定义,同时也提升了开发体验。你可以根据需要继续扩展 operations 对象,而不必担心手动维护类型。如果你有其他问题或需要进一步的帮助,请随时告诉我!

049 Satisfies (1)

在这个步骤中,我们将优化我们对运算符类型的管理,使其更具灵活性,同时保持类型安全和良好的开发体验。我们将通过使用 TypeScript 的 satisfies 语法来实现这一点。

目标

  1. 消除单独的运算符类型:避免因使用单独的运算符类型而导致的限制。
  2. 利用 satisfies 语法:使 operations 对象能够满足更广泛的类型,而无需重复类型定义。
  3. 保持类型安全和自动补全:确保在使用 operations 时仍能获得类型安全和 IDE 的自动补全功能。

示例实现

以下是更新后的代码示例:

import React from 'react';

// 定义运算类型
type Operation = (left: number, right: number) => number;

// 创建操作对象
const operations = {
    add: (a: number, b: number) => a + b,
    subtract: (a: number, b: number) => a - b,
    multiply: (a: number, b: number) => a * b,
    divide: (a: number, b: number) => a / b,
} satisfies Record<string, Operation>; // 使用 satisfies 语法

// 定义计算器属性接口
interface CalculatorProps {
    left?: number;
    operator?: keyof typeof operations; // 限制为 operations 对象的键
    right?: number;
}

// 计算器组件
const Calculator: React.FC<CalculatorProps> = ({
    left = 0,
    operator = 'add',
    right = 0,
}) => {
    const result = operations[operator](left, right);

    return (
        <div>
            <code>{`${left} ${operator} ${right}`}</code>
            = <output>{result}</output>
        </div>
    );
};

// 使用 Calculator 组件
const App = () => (
    <div>
        <Calculator left={1} right={2} operator="add" />
        <Calculator right={5} operator="multiply" />
        <Calculator left={4} />
        <Calculator />
    </div>
);

export default App;

代码解释

  1. 运算类型:定义了一个 Operation 类型,表示接收两个数字并返回一个数字的函数。

    type Operation = (left: number, right: number) => number;
  2. 使用 satisfies 语法:在 operations 对象中,使用 satisfies 关键字来指定 operations 应符合 Record<string, Operation> 类型。这样,operations 可以是任意字符串映射到 Operation 类型的对象,同时保持对各个操作的类型检查和自动补全功能。

    const operations = {
        add: (a: number, b: number) => a + b,
        subtract: (a: number, b: number) => a - b,
        multiply: (a: number, b: number) => a * b,
        divide: (a: number, b: number) => a / b,
    } satisfies Record<string, Operation>;
  3. 限制运算符:通过 keyof typeof operations,我们确保 operator 属性只能是 operations 对象中的有效键,从而增强了类型安全。

    operator?: keyof typeof operations; 

关键点

  • 消除冗余类型:通过使用 satisfies,我们避免了单独定义运算符类型,从而减少了冗余代码。
  • 类型安全和自动补全:在 operations 中保持了类型检查和 IDE 的自动补全功能,确保开发过程顺利。

结论

通过这个步骤,我们不仅优化了运算符的类型管理,还提升了代码的可维护性和可扩展性。你可以根据需要继续扩展 operations 对象,而不必担心手动维护类型。如果你有其他问题或需要进一步的帮助,请随时告诉我!

050 爸爸笑话时间 TypeScript

这是一个很好的时刻来休息一下!给自己一些时间来补充水分,放松心情,帮助你更好地消化和巩固所学的知识。

在学习新内容时,特别是编程和技术方面,休息和自我照顾是非常重要的。就像你提到的,写下你所学到的东西,即使不再查看,写的过程本身也会帮助你记住这些信息。

当你准备好继续时,欢迎回来,我们将继续学习更多有趣的内容!如果你有任何问题或者需要进一步的帮助,请随时告诉我。

051 样式简介

好了,在这个练习中,我们将进行样式的学习。

我现在应该戴上太阳镜,但没关系,Cody 是我的风格。

我们将讨论两种主要的样式应用方式:你可以使用内联样式或者外部 CSS 样式表(级联样式表)。通常,你会通过类名来定位元素。虽然你也可以通过属性或标签名来定位元素,这也是一个合理的做法,但我们将重点关注类名。

在 HTML 中,我们有一个类名为 my class。而在 JavaScript 中,你实际获取的是 className。需要注意,属性与特性不同,在 TypeScript 或 TSX(TypeScript 和 JSX 的混合)中,你使用的是属性名而不是特性名,所以是 className

关于样式,内联样式虽然不常用,但在某些情况下确实很有用。在 HTML 中,你会使用一长串 CSS 字符串,这种方式的功能有限,但这就是它的用法。而在 JavaScript 中,你会使用 CSS 样式对象。元素上有一个 style 属性,CSS 属性会以驼峰命名法来代替 CSS 中的连接符命名法。在 JSX 中,我们同样会使用 style,并传递一个对象,其中的属性是驼峰命名的。

另一个有趣的点是,你可以省略像 px 这样的单位。如果你为某个需要像素单位的属性只提供一个数值,那么 JSX 会自动为你添加 px 单位。当然,也可以是 rem 或其他单位。

还有一个点,初学 React 时经常会让人困惑,那就是双层大括号。其实这并没有什么特别之处,你可以通过定义一个对象并传递它作为插值的值来实现相同的效果。这是因为 JSX 允许插入 JavaScript 代码,而大括号中的内容正是一个 JavaScript 对象,因此双层大括号本质上只是对象的插值形式。

关于类名和内联样式,内联样式是直接应用在你渲染的元素上,而类名、属性和标签名则需要在页面中添加一个样式表。我们会在 HTML 中使用 href 来引入一个外部样式表,并在该样式表中定义 my class 类。另一种方式是将样式直接写在 HTML 的 style 标签中,不过这种方式使用较少。

虽然在 TSX 中有一些不同的处理方式,但我们很少这么做,因此不需要过多担心。

这应该足够让你开始这个练习了,我相信你会享受为应用添加样式的过程。

在这个练习中,另一个很酷的地方是我们将学习如何组合样式。早期内联样式曾一度流行,因为它们很容易组合在一起。然而,现在我们有了像 Tailwind 这样更好的工具,所以我们不会深入探讨 Tailwind,但我们会讨论如何组合 CSS 类名和内联样式属性。

当我们完成后,你将得到一个非常通用的盒子组件,它的 API 非常简洁,能让用户轻松创建出像这样的漂亮盒子。我相信你会喜欢这个练习。

052 样式

应用 CSS 样式

我们现在有一个 index.css 文件,其中包含了很多很酷的样式,这些样式我们希望应用到对应的元素上。

如果我们查看页面源代码,可以看到它作为样式表被引入。这里是指向 index.csshref

任务说明

你的任务是应用其中的一些类名,同时也要应用一些内联样式。

完成之后,页面应该看起来像这样(展示的最终效果)。希望你能在这个过程中享受乐趣!

053 样式 (1)

多光标操作魔法

我们将进行一些非常酷的多光标操作魔法。

首先,我会选择所有这些元素,然后为它们添加 className="box",接着将其设置为 box--these。其中有一个元素是无尺寸的(sizeless),我们将其去掉。

设置样式

接着,我们会添加 style 属性,设置 backgroundColor。背景颜色应该是这样的:backgroundColor。Boom!

然后我们将字体样式设置为斜体(italic)。Boom!看这个效果。

接下来更新这个元素,它并不是没有颜色的。这个类没有尺寸,但看这个效果,简直是神奇的——它有效!

处理类名和样式

每个元素都需要有一个 className,并且每个元素都需要有一个 style。即使是无尺寸、无颜色的 box,它依然应该有 className="box"。虽然它不需要尺寸或颜色,但它应该仍然是斜体的。

应用所有样式

这样,我们就为不同的元素应用了所有这些很棒的 CSS 属性。如果你成功实现了这一点,干得漂亮!

054 自定义组件

### 提取公共部分到 Box 组件

我们现在有一些重复的代码存在于不同的 box 组件中。我认为将这些公共部分提取到一个单独的 `Box` 组件中会非常有帮助。

目前,我们在 `div` 元素中指定了 `className="box"`,所有的 box 都有相同的 `className`。我们还设置了 `style` 属性,将字体样式定义为斜体(italic),所有的 box 都使用了相同的斜体样式。唯一的区别是类名和背景颜色,当然,子元素(children)也是一个 `prop`### 创建 Box 组件

我们可以创建一个 `Box` 组件,它接受这些 `props`,然后将它们组合在一起。这样,我们就可以获得正确的最终结果,而不需要每次重复定义所有公共的样式。

```tsx
interface BoxProps {
  className?: string;
  style?: React.CSSProperties;
  children?: React.ReactNode;
}

const Box: React.FC<BoxProps> = ({ className = '', style = {}, children }) => {
  return (
    <div
      className={`box ${className}`}
      style={{ fontStyle: 'italic', ...style }}
    >
      {children}
    </div>
  );
};

传递子元素(Children)

子元素(children)会通过 props 传递给 Box 组件中的 div 元素。我们可以像这样使用 Box 组件:

<Box className="custom-box" style={{ backgroundColor: 'lightblue' }}>
  这是一个自定义的 box 内容。
</Box>

这个 Box 组件会自动应用字体样式为斜体,同时允许我们自定义 classNamestyle 属性。

解构 Props

我们也可以通过解构 props 来处理额外的属性,比如 size

interface BoxProps {
  className?: string;
  style?: React.CSSProperties;
  children?: React.ReactNode;
  size?: 'small' | 'medium' | 'large'; // 新增的 size 属性
}

const Box: React.FC<BoxProps> = ({ className = '', style = {}, children, size }) => {
  const sizeStyle = size === 'small' ? { padding: '5px' } :
                    size === 'medium' ? { padding: '10px' } :
                    { padding: '20px' };

  return (
    <div
      className={`box ${className}`}
      style={{ fontStyle: 'italic', ...sizeStyle, ...style }}
    >
      {children}
    </div>
  );
};

结论

通过这个 Box 组件,你可以轻松复用公共样式,同时可以自定义类名、样式和额外的属性。接下来,你可以尝试为 Box 组件添加更多功能并进行扩展。

使用示例:

<Box className="large-box" style={{ backgroundColor: 'lightgreen' }} size="large">
  这是一个大号的 box 内容。
</Box>

这样就可以在应用中灵活使用 Box 组件,简化代码并提高可复用性。


这段代码展示了如何创建一个通用的 `Box` 组件,允许你复用公共样式并支持自定义类名和样式,同时还支持额外的 `size` 属性。

## 055 自定义组件 (1)  

### 创建 Box 组件

我们将创建一个函数组件 `Box`,并传递所有的 `props`。

```tsx
const Box: React.FC<React.ComponentProps<'div'>> = (props) => {
  return <div {...props} />;
};

在这个基础上,我们可以将所有的 div 元素更新为 Box 组件,这样我们就不必担心其他问题,所有东西仍然能正常工作。

自定义 ClassName 和 Style

我们可以做一些特殊的处理,尤其是在 classNamestyle 属性上。首先,我们从 props 中提取 className

const Box: React.FC<React.ComponentProps<'div'>> = ({ className = '', ...props }) => {
  return <div className={`box ${className}`} {...props} />;
};

你会注意到,虽然我们传递了 className="box small",但实际渲染时只显示了 box,这是因为我们在提取 className 后没有将它合并。因此,我们需要使用字符串插值来合并 className

处理多次重复的 ClassName

现在 className 已经合并完成,不过我们发现有时会重复出现 box。我们可以通过删除不必要的 box 来解决这个问题:

const Box: React.FC<React.ComponentProps<'div'>> = ({ className = '', ...props }) => {
  const combinedClassName = className ? `box ${className}` : 'box';
  return <div className={combinedClassName} {...props} />;
};

这样可以避免出现 undefined 或重复的 box,并且我们还可以移除所有多余的 box 类名。

合并 Style

另一个我们想要处理的是 style 属性。我们可以从 props 中提取 style,然后进行合并:

const Box: React.FC<React.ComponentProps<'div'>> = ({ className = '', style = {}, ...props }) => {
  const combinedClassName = className ? `box ${className}` : 'box';
  const combinedStyle = { fontStyle: 'italic', ...style };
  return <div className={combinedClassName} style={combinedStyle} {...props} />;
};

在这个例子中,fontStyle: 'italic' 是默认样式,用户传递的 style 会覆盖默认样式。我们还可以将 style 属性中的其他属性合并在一起。

进一步定制

现在你可以根据需求定制 Box 组件。可以使用这个组件创建不同的盒子元素:

<Box className="small-box" style={{ backgroundColor: 'lightblue' }}>
  这是一个小盒子。
</Box>
<Box className="medium-box" style={{ fontWeight: 900 }}>
  这是一个加粗字体的盒子。
</Box>

结论

通过这种方式,我们能够创建一个通用的 Box 组件,该组件能够合并 classNamestyle,并允许定制和扩展。这个模式非常常见,可以让你更轻松地管理和复用组件的样式和行为。

如果你对这些操作感到困惑,可以通过 console.log 打印 props 进行调试。这不仅有助于你理解 React 中的组件逻辑,还可以提升你的 JavaScript 技能。

希望你享受这个过程并觉得有趣!

056 尺寸Props

添加 Size 属性

在这一步,我们将进一步提升用户在使用 Box 组件时的体验。之前,用户需要手动传递 className,比如 box--small 或者 box--medium。现在,为了让用户的开发体验更好,我们将添加一个 size 属性,并为其定义具体的类型。

这样,开发者可以通过传递一个简单的 size 属性来控制盒子的大小,而不需要知道具体的类名。

我们依然保留 styleclassName 的组合方式,这样如果用户想要进一步覆盖或组合样式,他们依然可以这么做。

实现代码

我们将为 Box 组件添加一个新的 size 属性,并通过类型定义限制可选的尺寸。

interface BoxProps extends React.ComponentProps<'div'> {
  size?: 'small' | 'medium' | 'large'; // 定义 size 属性
}

const Box: React.FC<BoxProps> = ({ className = '', size, style = {}, ...props }) => {
  // 根据 size 属性来设置 className
  const sizeClass = size ? `box--${size}` : '';
  const combinedClassName = `box ${sizeClass} ${className}`.trim(); // 合并 className
  
  // 合并 style
  const combinedStyle = { fontStyle: 'italic', ...style };
  
  return (
    <div className={combinedClassName} style={combinedStyle} {...props} />
  );
};

使用示例

通过传递 size 属性,用户可以很轻松地控制盒子的大小,而无需了解底层的 CSS 类名。

<Box size="small" style={{ backgroundColor: 'lightblue' }}>
  这是一个小盒子。
</Box>

<Box size="large" className="custom-box" style={{ backgroundColor: 'lightgreen' }}>
  这是一个大盒子。
</Box>

在这个示例中,size="small" 会自动为 Box 组件应用 box--small 类名,而 size="large" 则会应用 box--large 类名。

结论

通过添加 size 属性,我们不仅简化了开发者的操作,还提高了 Box 组件的可扩展性和易用性。开发者现在只需传递一个 size 属性,就可以轻松控制盒子的大小,而无需担心具体的 CSS 类名。这种优化提供了更好的开发体验。

通过这种方式,用户可以轻松使用 size 属性来控制盒子的大小,同时保留了对 classNamestyle 的灵活性。

057 尺寸Props (1)

通过 Size 属性控制 Box 大小

在这一步,我们希望通过 size 属性来控制 Box 组件的大小,并为用户提供更好的开发体验。例如,用户可以通过简单的 size 属性来设置盒子的大小,比如 smallmediumlarge,而不需要手动传递类名。

实现代码

我们首先为 size 添加一个类型,并处理它的默认值和应用逻辑。

interface BoxProps extends React.ComponentProps<'div'> {
  size?: 'small' | 'medium' | 'large'; // 定义 size 属性
}

const Box: React.FC<BoxProps> = ({ className = '', size, style = {}, ...props }) => {
  // 根据 size 属性生成相应的类名
  const sizeClass = size ? `box--${size}` : '';
  
  // 使用数组存储类名并过滤掉空值
  const combinedClassName = [ 'box', sizeClass, className ]
    .filter(Boolean) // 过滤掉 falsy 值,如空字符串
    .join(' '); // 用空格拼接类名

  // 合并样式
  const combinedStyle = { fontStyle: 'italic', ...style };

  return <div className={combinedClassName} style={combinedStyle} {...props} />;
};

处理空类名的额外空格

为确保类名中没有多余的空格,我们将类名存储在一个数组中,并使用 filter(Boolean) 来过滤掉空字符串或 undefined。最后通过 join(' ') 将它们用空格拼接在一起。

使用示例

通过设置 size 属性,用户可以轻松控制 Box 的大小,而无需了解底层的类名。

<Box size="small" style={{ backgroundColor: 'lightblue' }}>
  这是一个小盒子。
</Box>

<Box size="medium" className="custom-box" style={{ backgroundColor: 'lightgreen' }}>
  这是一个中号盒子。
</Box>

<Box size="large" style={{ backgroundColor: 'pink' }}>
  这是一个大盒子。
</Box>

自动补全和更好的开发体验

通过为 size 属性定义明确的类型,开发者可以在使用时享受更好的自动补全支持。如果开发者错误地拼写了 size 的值,TypeScript 也会进行相应的类型检查,帮助开发者更快地发现问题。

<Box size="large">
  这是一个大盒子。
</Box>

结论

通过这种方式,Box 组件的样式逻辑完全自包含,用户只需要提供 size 属性即可控制组件的外观。我们还可以为开发者提供明确的 API,而不需要他们了解内部的类名或样式细节。这种模式提供了更好的开发体验,并确保了代码的可维护性。

而且,组件开发者可以完全控制哪些样式可以被覆盖,比如,如果你不希望用户覆盖 fontStyle,你可以固定该样式,使其不可被修改。

这种方法不仅简化了使用,还让组件变得更加灵活和可扩展。

通过这种实现,Box 组件的大小可以通过 size 属性轻松控制,且用户不需要关心底层的 CSS 类名,从而提供了更好的开发体验。

058 爸爸笑话时间 样式

提醒:让学习更加有效

用坏了的铅笔写字是毫无意义的,哈哈。

而如果你不能记住所学的内容,学习也是毫无意义的。所以,也许你可以拿出一支铅笔,把所学的东西记下来,这样你就能记住它们了。

然后,出去走动一下,让血液流动起来,这样你可以回来继续学习更多有趣的内容。

你的大脑需要休息,你也需要。希望你有一个愉快的休息时间,我们等你回来继续学习。

059 表单简介

介绍:进入表单的世界

现在是时候深入了解表单了!表单让你能够与网页互动,真的很神奇。

自从 HTML 在 90 年代中期标准化以来,表单就一直存在,并且可以做很多很酷的事情。我们将学习表单的一些基础知识,甚至还会添加一些事件处理程序,尽管我们还没有深入学习 React 的状态管理,但我们仍然会在表单和表单数据上进行一些很酷的操作。

表单基础

你可以在 MDN 上学习到有关表单的所有知识,那里有很多有趣的内容。表单最常见的元素是 input 元素,它可以有多种类型,从颜色选择器到日期选择器等等,我们将探索其中的许多内容。

事件处理

在网页平台上,当用户输入数据时,浏览器会触发事件,这样你的 JavaScript 就可以监听这些事件并执行相应的操作。在 React 中,我们使用 onChange 事件,而不是原生的 onInput。虽然 onChange 事件在 React 中处理得很像 onInput,但它实际上是等到输入框失去焦点时才会触发,这是一个小小的区别。

提交表单时防止页面刷新

在默认情况下,当你提交表单时,浏览器会触发完整的页面刷新。如果你不希望页面刷新,比如在构建像 X(Twitter)这样的应用时,每次点赞都刷新页面显然不理想。为避免这种情况,你可以在 onSubmit 事件中调用 preventDefault() 来阻止页面刷新。

const handleSubmit = (event: React.FormEvent) => {
  event.preventDefault();
  // 处理表单数据
};

为输入框关联标签

关联输入框与其标签是非常重要的。通常,标签可以包裹输入框,也可以使用 htmlFor 属性将标签与具有相同 id 的输入框关联起来。这有助于提高表单的可访问性。

<label htmlFor="username">用户名:</label>
<input id="username" type="text" />

总结

表单是一个庞大的主题,涉及的内容非常多。在本次练习中,我们将探索表单的许多基本功能,并学习如何通过 React 处理事件和表单数据。相信你会在这个过程中学到很多,并且乐在其中!

这段内容引导用户了解表单的基础知识,涵盖了事件处理、表单提交和标签关联等概念,同时使用了合适的代码示例。

060 表单

从头开始构建表单

我们现在从零开始,你的任务是将当前的待办事项(to-do)渲染成一个完整的表单。这张表单应该包含以下部分:

  • 用户名输入框
  • 提交按钮

当你在输入框中输入内容并点击提交时,页面会执行一次完整的刷新。你会注意到,表单数据会显示在 URL 中(我们会在下一步中讨论这一点)。

任务目标

  1. 创建包含用户名输入框和提交按钮的表单。
  2. 实现提交时的页面刷新功能。

额外任务

  • 确保当点击标签时,输入框会获得焦点,确保标签正确关联到输入框。

相信你现在已经拥有足够的信息来开始这个任务了,动手试试吧!

这段内容引导你从头构建一个简单的表单,同时解释了页面刷新和 URL 传递表单数据的行为,并提供了一些额外的挑战,如正确关联标签和输入框。

061 表单 (1)

渲染一个基础表单

我们将从渲染表单开始,首先创建一个基本的表单结构。

return (
  <form>
    <label htmlFor="username-input">用户名:</label>
    <input id="username-input" type="text" />
    <button type="submit">提交</button>
  </form>
);

设置按钮类型

尽管不是强制要求的,但我通常会明确地为按钮设置 type 属性,尤其是在复杂应用中。如果没有明确设置按钮类型,可能会在不知情的情况下提交父级表单。所以在这例中,我们将按钮的 type 设置为 submit

无障碍功能测试

如果我们没有设置 htmlForid,当你点击标签时,输入框并不会获得焦点,而且屏幕阅读器也不会为输入框提供有意义的提示。因此,添加 idhtmlFor 属性是确保无障碍访问的重要步骤。

通过设置 htmlFor="username-input",并在输入框上添加 id="username-input",我们可以确保点击标签时,输入框会自动聚焦,同时屏幕阅读器能够正确读取标签的内容。

<label htmlFor="username-input">用户名:</label>
<input id="username-input" type="text" />

提交表单并查看 URL

当你输入用户名并点击提交按钮时,页面会执行完整的刷新,提交的数据会显示在 URL 中。我们稍后会详细讨论这一点。

<form>
  <label htmlFor="username-input">用户名:</label>
  <input id="username-input" type="text" />
  <button type="submit">提交</button>
</form>

小结

通过本次练习,我们实现了一个基本的表单,确保了无障碍访问,并能够提交数据。你可以进一步优化样式,确保表单更符合实际需求。当提交表单时,数据会在 URL 中显示,我们会在后续的步骤中探讨如何处理这些数据。希望你已经完成了这个目标!

这个示例引导用户创建一个基本的表单,并介绍了设置 htmlForid 以确保无障碍访问,同时展示了如何处理表单提交。

062 表单操作

理解表单的默认提交行为

当我们提交表单时,发生了一个完整的页面刷新。这是因为我们向服务器发送了请求,表单数据被序列化并附加到当前 URL 中。例如,当我输入用户名为 Kent C. Dodds 并点击提交时,浏览器会在 URL 中生成查询参数 ?username=Kent+C.+Dodds,并触发页面刷新。

默认行为:提交到当前 URL

默认情况下,表单会向当前的 URL 发起请求。如果你想控制请求发送到的具体路径,可以使用表单的 action 属性。action 属性允许你指定请求发送到的服务器端点。

<form action="/submit-url">
  <!-- 表单内容 -->
</form>

任务说明

在本次练习中,我们有一个简单的后端 api.server 来处理表单提交。它已经为你设置好了 loader,负责处理 GET 请求,并将表单数据传递给处理函数。在本次练习中,你的任务是为表单添加 action,并查看提交后的效果。

示例

<form action="/submit-form">
  <label htmlFor="username-input">用户名:</label>
  <input id="username-input" type="text" name="username" />
  <button type="submit">提交</button>
</form>

小结

通过设置 action 属性,你可以控制表单提交请求的路径。在这个练习中,设置 action 后,表单数据会发送到指定的路径,你可以通过后端处理这个请求并响应数据。试着为你的表单添加 action 属性并查看提交后的效果,提交应该非常快速完成。

这段内容解释了表单的默认提交行为,并引导你通过设置 action 属性来控制请求发送的路径。通过简单的示例代码,帮助你理解如何在实际开发中实现该功能。

063 表单操作 (1)

添加 Action 属性来配置表单提交路径

现在,我们将为表单添加一个 action 属性,该属性设置为 /api/onboarding,用于指定表单提交时的请求路径。

<form action="/api/onboarding">
  <label htmlFor="username-input">用户名:</label>
  <input id="username-input" type="text" name="username" />
  <button type="submit">提交</button>
</form>

通过 URL 序列化表单数据

当你输入用户名并点击提交时,表单数据会被序列化为 URL 参数。例如,如果你输入了用户名 "JohnDoe",那么提交后的 URL 会变为:

/api/onboarding?username=JohnDoe

后端响应

当表单数据被提交到 /api/onboarding,我们的后端会处理请求,并返回一个包含所有序列化值的 HTML 文档,方便我们查看。

使用绝对路径和相对路径

你可以将 action 设置为绝对路径或相对路径。绝对路径可能类似于 /app-playground/api/onboarding,而相对路径则是 api/onboarding。两者都会工作,只是取决于你想将请求发送到哪个路径。

<form action="/app-playground/api/onboarding">
  <!-- 表单内容 -->
</form>

小结

action 属性决定了表单数据提交的路径。你可以根据需要配置相对或绝对路径。当表单提交时,数据将被序列化并发送到指定的 URL。通过这种方式,你可以轻松控制表单的提交行为。

这个示例解释了如何通过 action 属性设置表单的提交路径,并展示了如何在 URL 中序列化表单数据并将其提交到服务器。

064 输入类型

增强我们的表单

目前我们的表单只有用户名(username),但我们现在要进一步增强它,添加以下字段:

  • 密码(password)
  • 年龄(age)
  • 照片(photo)
  • 最喜欢的颜色(favorite color)
  • 开始日期(start date)

这些输入类型的详细信息可以在 MDN(Mozilla Developer Network)文档中找到。MDN 提供了各种输入类型的指南,帮助你理解它们如何在表单中工作。

示例表单

<form action="/submit">
  <label htmlFor="username">用户名:</label>
  <input id="username" type="text" name="username" />

  <label htmlFor="password">密码:</label>
  <input id="password" type="password" name="password" />

  <label htmlFor="age">年龄:</label>
  <input id="age" type="number" name="age" />

  <label htmlFor="photo">照片:</label>
  <input id="photo" type="file" name="photo" />

  <label htmlFor="favorite-color">最喜欢的颜色:</label>
  <input id="favorite-color" type="color" name="favoriteColor" />

  <label htmlFor="start-date">开始日期:</label>
  <input id="start-date" type="date" name="startDate" />

  <button type="submit">提交</button>
</form>

探索不同的输入类型

我们添加了各种类型的输入,包括文本框、密码框、数字输入、文件上传、颜色选择器和日期选择器。每种输入类型会影响表单提交时的数据格式。例如:

  • type="password" 会隐藏用户输入的内容。
  • type="file" 会允许用户上传文件。
  • type="color" 会显示颜色选择器。
  • type="date" 会让用户选择日期。

小结

通过添加多种输入类型,我们增强了表单的功能,并且不同的输入类型在表单提交时会影响表单数据的格式。你可以参考 MDN 文档来深入了解每种输入类型的用法,并尝试不同的组合来提升表单的交互体验。

这个增强版表单添加了多种不同类型的输入字段,并解释了每种输入类型的作用及其对表单提交的影响。通过这个示例,你可以了解如何在表单中应用多种输入类型。

065 输入类型 (1)

增强表单:更多输入类型

我们继续改进我们的表单,现在已经有用户名输入框了。接下来我们要添加以下字段:

  • 密码(password)
  • 年龄(age)
  • 照片上传(photo)
  • 最喜欢的颜色(favorite color)
  • 开始日期(start date)

我们将通过添加这些不同类型的输入,来了解它们如何影响表单提交。

添加字段

  1. 密码输入框

    • 使用 type="password" 确保输入的内容不会明文显示。
  2. 年龄输入框

    • 使用 type="number" 以确保用户只能输入数字。
  3. 照片上传

    • 使用 type="file",并通过 accept="image/*" 限制用户只能上传图片文件。
  4. 颜色选择器

    • 使用 type="color",允许用户选择自己喜欢的颜色。
  5. 开始日期

    • 使用 type="date",让用户选择日期,虽然可能不是设计师理想的样式,但非常适合 MVP。

示例表单

<form action="/submit-form" method="get">
  <label htmlFor="username">用户名:</label>
  <input id="username" type="text" name="username" />

  <label htmlFor="password">密码:</label>
  <input id="password" type="password" name="password" />

  <label htmlFor="age">年龄:</label>
  <input id="age" type="number" name="age" />

  <label htmlFor="photo">照片:</label>
  <input id="photo" type="file" name="photo" accept="image/*" />

  <label htmlFor="favorite-color">最喜欢的颜色:</label>
  <input id="favorite-color" type="color" name="favoriteColor" />

  <label htmlFor="start-date">开始日期:</label>
  <input id="start-date" type="date" name="startDate" />

  <button type="submit">提交</button>
</form>

提交数据与结果

当你输入所有信息并点击提交按钮后,页面会进行完整的刷新,并将表单数据以查询参数的形式添加到 URL 中。上传的图片文件只会传递文件名,而非实际的文件内容,这需要进一步的处理。

密码字段的特别之处

注意,当你点击浏览器的“返回”按钮时,所有的字段内容都会保留,除了密码。这是因为密码字段由于安全原因在浏览器中具有特殊的行为,它不会在返回后保留内容。

总结

通过增强表单,我们添加了多种不同类型的输入字段,并理解了它们对用户体验的影响。未来我们将进一步处理照片上传的细节问题,敬请期待。

通过这次改进,我们将多个不同类型的输入字段添加到了表单中,并解释了每个字段的作用及其对表单提交的影响。这些字段有助于增强用户体验,同时为后续处理照片上传等高级功能奠定了基础。

066 提交

解决表单中的问题

我们在使用表单时发现了两个主要问题:

  1. 密码暴露在 URL 中:当提交表单时,密码会作为查询参数出现在 URL 中,这是不安全的做法。
  2. 图片上传问题:当前表单只发送了图片的文件名,而不是实际的图片数据,这并不能满足上传需求。

此外,表单每次提交都会导致页面刷新,这是我们希望避免的。

问题 1:防止密码出现在 URL 中

表单默认通过 GET 请求将数据序列化并附加到 URL 中,这对密码字段来说非常不安全。我们可以通过将表单的 method 属性设置为 POST 来解决此问题。POST 请求不会将数据附加到 URL 中,而是通过请求体发送。

<form action="/submit-form" method="post">
  <label htmlFor="username">用户名:</label>
  <input id="username" type="text" name="username" />

  <label htmlFor="password">密码:</label>
  <input id="password" type="password" name="password" />

  <!-- 其他输入字段 -->
  <button type="submit">提交</button>
</form>

问题 2:处理图片上传

对于文件上传,我们需要确保表单使用 enctype="multipart/form-data" 来正确传输二进制数据(如图片)。默认的 enctype 只支持发送简单的文本数据。

<form action="/submit-form" method="post" enctype="multipart/form-data">
  <label htmlFor="photo">照片:</label>
  <input id="photo" type="file" name="photo" accept="image/*" />

  <!-- 其他输入字段 -->
  <button type="submit">提交</button>
</form>

问题 3:防止页面刷新

我们还希望在提交表单时避免页面刷新。为此,可以通过在 onSubmit 事件处理程序中调用 event.preventDefault() 来阻止默认的表单提交行为。

<form id="my-form" action="/submit-form" method="post" enctype="multipart/form-data">
  <label htmlFor="username">用户名:</label>
  <input id="username" type="text" name="username" />

  <label htmlFor="password">密码:</label>
  <input id="password" type="password" name="password" />

  <label htmlFor="photo">照片:</label>
  <input id="photo" type="file" name="photo" accept="image/*" />

  <button type="submit">提交</button>
</form>

<script>
  document.getElementById('my-form').addEventListener('submit', function(event) {
    event.preventDefault();
    // 处理自定义表单提交逻辑
  });
</script>

逐步实现

  1. 添加 method="post" 以防止密码出现在 URL 中
  2. 添加 enctype="multipart/form-data" 以正确处理文件上传
  3. 通过 event.preventDefault() 防止表单提交导致页面刷新

在每一步之后,可以测试表单的行为,并观察提交时数据的变化。最后一步,我们会将这些逻辑与后端 API 结合,查看它们如何协同工作。

总结

通过这次练习,我们解决了表单提交时密码暴露和图片上传的问题,并防止了页面刷新。随着你添加每个属性和事件处理程序,你可以看到表单行为的变化。希望你在这次练习中有所收获!

这段内容详细介绍了如何解决表单中的常见问题,特别是防止密码暴露在 URL 中、正确处理图片上传以及防止页面刷新。通过逐步实现,用户可以观察每个更改的影响。

067 提交 (1)

表单提交与 GET 请求

在默认情况下,表单通过 GET 请求提交,并将所有数据序列化到 URL 中。这在某些场景下是有用的,比如在电商网站上,你可能想将搜索条件保存在书签中,或与他人分享。但是,对于包含敏感信息的表单(如密码或上传的文件),这种方式显然不合适。

为什么需要使用 POST 请求

在我们的表单中,包含了密码和照片上传等敏感数据。如果使用 GET 请求,这些数据将出现在 URL 中,导致安全隐患。GET 请求的默认行为是将所有表单数据附加到 URL 中,因此我们需要使用 POST 请求来避免这一问题。

<form action="/submit-form" method="post">
  <label for="username">用户名:</label>
  <input id="username" type="text" name="username" />

  <label for="password">密码:</label>
  <input id="password" type="password" name="password" />

  <button type="submit">提交</button>
</form>

通过将表单的 method 设置为 post,表单数据将不会附加到 URL 中,而是通过请求体发送。这不仅保护了用户的隐私,还符合标准的安全实践。

API 修改:处理 POST 请求

当我们使用 POST 请求时,后端需要通过请求体来获取数据,而不是从 URL 中获取。在后端,我们使用 request.formData() 来处理请求体中的表单数据。

async function action({ request }) {
  const formData = await request.formData();
  const username = formData.get('username');
  const password = formData.get('password');
  // 处理表单数据...
}

通过 formData.get() 可以获取表单中的各个字段,并在后端进行处理。

上传文件与多部分表单数据

对于上传文件,我们需要将表单的 enctype 设置为 multipart/form-data,以便正确传输二进制数据(如图像文件)。默认的 application/x-www-form-urlencoded 只适用于简单的文本数据,而不能处理文件上传。

<form action="/submit-form" method="post" enctype="multipart/form-data">
  <label for="photo">上传照片:</label>
  <input id="photo" type="file" name="photo" accept="image/*" />

  <button type="submit">提交</button>
</form>

在设置了 multipart/form-data 后,表单可以处理文件上传,并将文件以二进制的形式传输到服务器。

避免页面刷新:使用 preventDefault()

为了避免提交表单时导致页面刷新,我们可以在 onSubmit 事件处理程序中调用 event.preventDefault() 来阻止默认的表单提交行为。

<form id="my-form" action="/submit-form" method="post" enctype="multipart/form-data">
  <label for="username">用户名:</label>
  <input id="username" type="text" name="username" />

  <label for="password">密码:</label>
  <input id="password" type="password" name="password" />

  <label for="photo">照片:</label>
  <input id="photo" type="file" name="photo" accept="image/*" />

  <button type="submit">提交</button>
</form>

<script>
  document.getElementById('my-form').addEventListener('submit', function(event) {
    event.preventDefault();
    // 处理自定义表单提交逻辑
  });
</script>

通过 preventDefault(),我们可以阻止页面刷新,并在客户端自行处理表单数据的逻辑。

前端解析表单数据

我们可以在客户端使用 FormData API 来解析表单数据并执行相关处理。例如,我们可以通过以下代码获取表单的所有数据,并以对象的形式输出:

document.getElementById('my-form').addEventListener('submit', function(event) {
  event.preventDefault();
  const formData = new FormData(event.target);
  const entries = Object.fromEntries(formData.entries());
  console.log(entries);
});

这段代码会将表单中的所有字段数据转换为对象格式,并输出到控制台。对于文件上传,FormData API 还支持处理文件对象,因此你可以进一步操作上传的文件。

总结

通过这次练习,我们了解了表单提交的几种方法以及如何通过 POST 请求和 multipart/form-data 来安全地处理表单数据。我们还探讨了如何在前端处理表单数据,并避免页面刷新。这些都是开发表单时非常重要的知识点。

通过这一段总结,我们解决了表单提交中密码暴露和文件上传的问题,同时还学习了如何在前端通过 preventDefault() 避免页面刷新并处理表单数据。

068 表单操作

React 表单处理的简化

在处理表单的提交、方法和编码时,常常需要进行许多配置工作,包括设置 actionencodingType 以及 onSubmit 事件。这些操作虽然可以手动完成,但在 React 中,有一种更简便的方式来处理表单。

React 提供了一些内置的功能,使得你不必重复实现这些常见的功能。本次练习中,我们将删除之前的额外配置,简化表单处理的方式,并且利用更简单的代码来完成表单的提交逻辑。

删除手动配置

首先,我们可以移除手动设置的 actionencodingTypeonSubmit 事件处理逻辑。接下来,我们只需传递一个函数给 onSubmit,这样我们就可以轻松管理表单的提交和处理。

示例代码

以下是一个使用 React 内置功能简化表单处理的示例代码:

import React, { useState } from 'react';

function SimpleForm() {
  const [formData, setFormData] = useState({
    username: '',
    password: '',
    photo: null,
  });

  const handleSubmit = (event) => {
    event.preventDefault();
    console.log('Submitted data:', formData);
    // 在这里处理提交逻辑,或者发送 API 请求
  };

  const handleChange = (event) => {
    const { name, value, files } = event.target;
    setFormData((prevData) => ({
      ...prevData,
      [name]: files ? files[0] : value,
    }));
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        用户名:
        <input
          type="text"
          name="username"
          value={formData.username}
          onChange={handleChange}
        />
      </label>
      <br />
      <label>
        密码:
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
        />
      </label>
      <br />
      <label>
        上传照片:
        <input
          type="file"
          name="photo"
          accept="image/*"
          onChange={handleChange}
        />
      </label>
      <br />
      <button type="submit">提交</button>
    </form>
  );
}

export default SimpleForm;

关键点解释

  1. handleSubmit 函数:这个函数阻止了表单的默认提交行为,并通过 event.preventDefault() 来防止页面刷新。随后,我们可以根据需求处理表单数据,譬如发送请求到服务器。

  2. handleChange 函数:这个函数会根据输入字段的变化动态更新表单的状态。对于 file 类型的输入,它会保存上传的文件对象,而不是其路径。

  3. 自动管理表单状态:我们通过 useState 钩子来管理表单的数据,使得每次输入都能够自动更新。

使用建议

通过这种方式,你可以轻松管理复杂的表单逻辑,无需担心手动处理编码类型或防止页面刷新。React 的组件化模型和状态管理极大简化了表单处理的流程。

这样,你就可以专注于实际的业务逻辑,而不用再为表单的基础设施烦恼。希望你能通过这一简化体验到 React 的便利性。

069 表单操作 (1)

React 表单处理的优化

在这部分内容中,我们了解到如何利用 React 内置功能来简化表单处理。通过传递一个函数给 action,React 能够处理很多繁琐的表单提交逻辑,并自动处理一些常见的操作,比如表单重置和防止默认行为。

代码重构示例

我们将通过重构代码,删除不必要的手动逻辑,利用 React 自动处理表单数据的能力。

import React from 'react';

function SimpleForm() {
  const logFormData = (formData) => {
    const entries = Object.fromEntries(formData.entries());
    console.log('Submitted form data:', entries);
  };

  return (
    <form action={logFormData}>
      <label>
        用户名:
        <input type="text" name="username" defaultValue="Kent" />
      </label>
      <br />
      <label>
        密码:
        <input type="password" name="password" />
      </label>
      <br />
      <label>
        年龄:
        <input type="number" name="age" defaultValue={30} />
      </label>
      <br />
      <button type="submit">提交</button>
    </form>
  );
}

export default SimpleForm;

核心逻辑

  1. logFormData 函数:我们创建了一个 logFormData 函数来接收并处理表单数据。表单数据以 formData 对象的形式传递给 action,并通过 Object.fromEntries 方法将其转换为普通对象,方便输出。

  2. action 属性:我们将 action 设置为 logFormData 函数。React 将自动处理表单提交时的各种操作,如阻止默认提交行为,获取表单数据,并传递给 logFormData 进行处理。

  3. 表单重置:React 自动处理表单提交后的重置行为。提交表单后,表单将会重置为默认值。

React 的优势

  • 减少手动操作:我们不再需要手动处理 onSubmitpreventDefault() 或其他繁琐的提交逻辑,React 内置的功能已经帮我们做了这些工作。

  • 自动重置表单:表单提交后,React 会自动重置表单为初始状态,避免用户重复输入相同信息。

注意事项

在表单重置时,如果你没有为某些字段设置默认值(defaultValue),你可能会看到浏览器的警告。此时,React 尝试将表单重置为其默认状态,但可能遇到无法重置为空值的问题。

结论

通过这种方式,React 让表单处理变得更加简洁和高效。你可以专注于表单数据的业务逻辑,而无需处理表单提交的底层细节。这种方式不仅提升了开发体验,还使代码更加简洁易维护。

070 爸爸笑话时间 表单

071 输入简介

在 React 中添加不同的输入类型

在本次练习中,我们将探讨各种输入类型及其在 React 中的处理细节。虽然很多信息可以在 MDN Web 文档中找到,但 React 引入了一些特定的行为,尤其是在处理 defaultValuevalue 属性时,这些是你需要掌握的关键。

我们将处理以下输入类型:

  • 选择框(Select dropdown) 用于选择账户类型。
  • 复选框(Checkbox) 用于控制可见性。
  • 其他具有 默认值 的输入字段。

关键区别:

  • defaultValue:当你希望设置一个初始值供用户修改时使用。
  • value:用于受控组件(controlled components),其输入值与组件的状态直接关联。
  • React 与 HTML 属性的差异:在 HTML 中,你可能会使用 value 属性,但在 React 中,通常会使用 defaultValue 来处理非受控组件,或者使用 value 来处理受控组件。

我们将探索的输入类型:

  • 选择框(Select):用于选择账户类型的下拉菜单。
  • 复选框(Checkbox):用于切换可见性。
  • 默认值:各种输入字段,其中我们将设置初始值。

接下来,我们将结合 Web 标准和 React 特有的行为,逐步理解这些输入类型在 React 中的工作原理。

072 复选框

在 React 中创建复选框和标签

在本次练习中,我们将为复选框创建一个标签并讨论如何根据设计需求摆放复选框及其对应的文本。

复选框通常会被放置在文本的旁边,例如复选框在前,文本在后。虽然在某些设计中,你可能希望以不同的方式来排布这些元素,但这种设置在很多情况下是常见的,特别是当你希望标签直接与复选框并排显示时。

复选框的实现步骤:

  1. 创建标签元素(<label>:将文本放置在标签内,复选框也嵌套其中。这样,当你点击文本时,也可以选择复选框。
  2. 定义输入类型为 checkbox:这是复选框的主要属性,不同于文本输入框(text),这里我们会使用 checkbox

以下是代码示例:

<label>
  <input type="checkbox" name="visibility" />
  显示内容
</label>

需要考虑的点:

  • 交互性:当你点击标签中的文本时,复选框也应该被选中或取消选中。将复选框嵌入标签中是一个常见的做法,可以实现这种交互。
  • 设计和布局:复选框和文本的排布方式应根据设计需求进行调整。你可以通过 CSS 来更改其布局和样式,使其符合你应用的 UI 规范。

小结:

通过这种方式,我们可以快速创建一个符合用户体验的复选框和标签布局。在实际应用中,你可以根据需要调整样式或布局来优化视觉效果和用户交互。

073 复选框 (1)

在 React 中创建复选框并处理表单数据

在本次练习中,我们继续探讨如何在 React 中创建复选框,并深入理解复选框在表单提交时的数据处理方式。

1. 创建复选框

首先,我们会在表单中创建一个复选框,并将标签置于复选框的右侧,这是一种常见的布局方式。复选框的 type 属性应设置为 "checkbox"

<div>
  <label>
    <input type="checkbox" name="waiver" />
    我已阅读并同意条款
  </label>
</div>

2. 提交表单时的复选框行为

当我们提交表单时,复选框的行为有两个关键点:

  • 选中时:如果复选框被选中,提交的数据中会包含复选框的 name 属性及其值为 "on"。也就是说,当复选框被选中时,复选框的 name 对应的值会是 "on",这与我们通常期望的 true/false 不同。
  • 未选中时:如果复选框没有被选中,表单数据中将不包含该复选框的 name。这意味着复选框未被选中时,不会在提交的数据中体现出来。
<form>
  <input type="text" name="username" placeholder="用户名" />
  <input type="checkbox" name="waiver" />
  <button type="submit">提交</button>
</form>

3. 表单数据展示

当你提交表单后,如果复选框被选中,表单数据中的 waiver 字段会显示为 "on"。如果没有选中,表单数据将不会包含 waiver 字段。这种行为是浏览器原生表单处理的一部分,与 React 无关。

4. 示例输出

当复选框被选中并提交时,表单数据可能如下:

{
  "username": "Kent C. Dodds",
  "waiver": "on"
}

如果复选框未选中,数据可能如下:

{
  "username": "Kent C. Dodds"
}

5. 结论

复选框在表单数据中的表示形式比较特殊,它在选中时会传递 "on" 作为值,而在未选中时则不会传递任何信息。这是 Web 平台的默认行为,了解这一点对你处理表单数据非常有帮助。在实际开发中,你可能会使用某些库来处理这类行为,但理解基础仍然很重要。

074 下拉选择

创建带有选择功能的下拉框

在本次练习中,我们将创建一个下拉选择框,用于表单中选择不同的用户类型。下拉框包含四种用户类型:管理员、老师、家长和学生。默认选项将是 "请选择一个选项",并且在提交时如果没有选择具体的用户类型,表单将返回一个空字符串。

1. 创建下拉选择框

我们将使用 selectoption 元素来实现下拉框选择功能。每个选项代表一个用户类型,包括管理员、老师、家长和学生。

<form>
  <label htmlFor="accountType">用户类型:</label>
  <select name="accountType" id="accountType">
    <option value="">请选择一个选项</option>
    <option value="admin">管理员</option>
    <option value="teacher">老师</option>
    <option value="parent">家长</option>
    <option value="student">学生</option>
  </select>
  <button type="submit">提交</button>
</form>

2. 提交表单时的处理

当表单提交时,如果用户没有选择任何有效选项(选择的是 "请选择一个选项"),表单数据中的 accountType 字段将为空字符串。如果用户选择了其他选项,比如 "管理员"、"老师" 等,提交的数据将会包含对应的值。

<form onSubmit={handleSubmit}>
  <label htmlFor="accountType">用户类型:</label>
  <select name="accountType" id="accountType">
    <option value="">请选择一个选项</option>
    <option value="admin">管理员</option>
    <option value="teacher">老师</option>
    <option value="parent">家长</option>
    <option value="student">学生</option>
  </select>
  <button type="submit">提交</button>
</form>

handleSubmit 函数中,我们可以处理选择框的数据:

const handleSubmit = (event) => {
  event.preventDefault();
  const formData = new FormData(event.target);
  const accountType = formData.get('accountType') || '';

  console.log('选择的用户类型:', accountType);
};

3. 表单数据的展示

  • 如果选择了有效的用户类型,如 "管理员",表单数据将返回 "admin"
  • 如果没有选择任何有效选项(即保持 "请选择一个选项"),accountType 将为空字符串。

4. 示例输出

  • 当选择 "管理员" 时,提交的数据可能是:
    {
      "accountType": "admin"
    }
  • 当没有选择有效选项时,提交的数据可能是:
    {
      "accountType": ""
    }

5. 结论

通过这次练习,你可以理解如何在 React 中使用 selectoption 元素创建下拉选择框,并处理其表单提交数据。掌握这些基础知识对构建动态表单应用非常重要。

075 下拉选择 (1)

使用 select 元素创建下拉菜单

在这次练习中,我们将使用 select 元素和 option 元素来创建一个下拉菜单,允许用户选择角色类型,比如管理员、教师、家长或学生。我们还会添加一个默认选项 "请选择一个选项",并确保当该选项被选择时,提交的表单数据将为空字符串。

1. 创建下拉菜单

使用 select 元素包裹多个 option 元素,每个 option 代表一个可供选择的角色类型。

<form>
  <label htmlFor="accountType">用户类型:</label>
  <select name="accountType" id="accountType">
    <option value="">请选择一个选项</option>
    <option value="admin">管理员</option>
    <option value="teacher">教师</option>
    <option value="parent">家长</option>
    <option value="student">学生</option>
  </select>
  <button type="submit">提交</button>
</form>

2. 处理表单提交

通过 handleSubmit 函数获取表单数据,并处理下拉菜单的值。确保在用户没有选择有效选项时,accountType 返回空字符串。

const handleSubmit = (event) => {
  event.preventDefault();
  const formData = new FormData(event.target);
  const accountType = formData.get('accountType') || '';
  console.log('选择的用户类型:', accountType);
};

3. 注意事项

  • 如果用户选择了 "请选择一个选项",表单提交时会返回一个空字符串。
  • 选择其他选项时,提交的值会是所选选项的 value,比如 "admin""teacher"

4. 输出结果

根据选择不同的用户类型,提交的数据如下所示:

  • 选择 "管理员":
    {
      "accountType": "admin"
    }
  • 没有选择有效选项:
    {
      "accountType": ""
    }

5. 结论

selectoption 元素提供了一种简单的方式来创建下拉菜单,用户可以通过选择不同的选项进行表单提交。根据顺序排列的 option 元素决定默认的显示值,开发者可以通过 value 属性自定义每个选项的提交值。

076 单选按钮

使用单选按钮 (Radio Buttons) 创建选择组

在这次练习中,我们将使用单选按钮(radio buttons)来创建一个只能选择一个选项的表单组。单选按钮与 select 类似,因为它们都只允许用户选择一个值;同时与复选框也有相似之处,因为你需要使用 checked 属性来判断某个选项是否被选中。

我们还将使用 fieldset 元素来包裹一组单选按钮,并用 legend 为整个选项组添加描述性标签。

1. 使用 fieldsetlegend

fieldset 元素用于包裹一组相关的表单元素,而 legend 用来为这组元素添加描述性标签。

<form>
  <fieldset>
    <legend>请选择您的角色</legend>

    <label>
      <input type="radio" name="role" value="admin" /> 管理员
    </label>

    <label>
      <input type="radio" name="role" value="teacher" /> 教师
    </label>

    <label>
      <input type="radio" name="role" value="parent" /> 家长
    </label>

    <label>
      <input type="radio" name="role" value="student" /> 学生
    </label>
    
    <button type="submit">提交</button>
  </fieldset>
</form>

2. 处理表单提交

通过 handleSubmit 函数获取表单数据,处理单选按钮的值。确保在用户选择不同选项时,能够正确获取选中的角色。

const handleSubmit = (event) => {
  event.preventDefault();
  const formData = new FormData(event.target);
  const role = formData.get('role');
  console.log('选择的角色:', role);
};

3. 注意事项

  • 单选按钮的 name 属性必须相同,这样才能确保用户在提交表单时只能选择一个选项。
  • checked 属性可以用来判断某个选项是否被选中。

4. 输出结果

根据选择不同的角色类型,提交的数据如下所示:

  • 选择 "管理员":
    {
      "role": "admin"
    }
  • 选择 "学生":
    {
      "role": "student"
    }

5. 结论

通过使用 fieldsetlegend 包裹一组单选按钮,用户可以在多个选项中选择一个,并且这些选项会有一个描述性标签。

077 单选按钮 (1)

使用单选按钮组和 fieldset 实现选择功能

在这个练习中,我们使用单选按钮和 fieldset 来创建一个可供用户选择的分组。fieldsetlegend 的作用是为这组单选按钮提供一个整体的标签,确保屏幕阅读器和其他辅助技术可以正确理解这些选项的含义。

1. 使用 fieldsetlegend

fieldset 元素包裹一组相关的表单元素,而 legend 用来为这组元素添加描述性标签:

<form>
  <fieldset>
    <legend>选择可见性</legend>

    <label>
      <input type="radio" name="visibility" value="public" /> 公开
    </label>
    
    <label>
      <input type="radio" name="visibility" value="private" /> 私密
    </label>

    <label>
      <input type="radio" name="visibility" value="unlisted" /> 未列出
    </label>

    <button type="submit">提交</button>
  </fieldset>
</form>

2. 表单提交处理

通过 handleSubmit 函数处理表单数据,判断用户选择了哪个可见性选项:

const handleSubmit = (event) => {
  event.preventDefault();
  const formData = new FormData(event.target);
  const visibility = formData.get('visibility');
  console.log('选择的可见性:', visibility);
};

3. 注意事项

  • 所有单选按钮必须具有相同的 name,这使得用户在提交表单时只能选择一个选项。
  • value 属性决定了提交的表单数据中与该单选按钮相关联的值。

4. 测试结果

提交表单时,如果选择了 "公开":

{
  "visibility": "public"
}

选择 "未列出":

{
  "visibility": "unlisted"
}

如果未选择任何选项,表单数据中将不会包含 visibility 键。

5. 结论

通过使用 fieldsetlegend,我们可以为一组单选按钮提供一个整体的描述性标签。这样,用户和辅助技术都可以更清楚地知道这些选项是为哪个设置服务的。

078 隐藏输入

使用隐藏输入字段(Hidden Input)向服务器提交额外数据

在某些情况下,您需要将一些额外的数据提交给服务器,但不希望用户手动输入。这时可以使用隐藏的输入字段 (hidden input)。这些字段不会显示在用户界面上,但会随表单数据一起提交。

1. 使用隐藏的输入字段

隐藏输入字段通过 type="hidden" 实现。它的作用是存储一些信息,这些信息不会显示给用户,但在表单提交时会被发送到服务器。

<form>
  <label>
    任务名称:
    <input type="text" name="taskName" />
  </label>
  
  <input type="hidden" name="taskId" value="12345" />

  <button type="submit">提交</button>
</form>

2. 提交处理

当用户提交表单时,taskId 作为隐藏输入字段将随表单一起发送到服务器:

const handleSubmit = (event) => {
  event.preventDefault();
  const formData = new FormData(event.target);
  const taskId = formData.get('taskId');
  const taskName = formData.get('taskName');
  console.log('任务 ID:', taskId);
  console.log('任务名称:', taskName);
};

3. 用例场景

隐藏输入字段可以在以下情况下使用:

  • 提交表单时传递额外的上下文数据:如用户 ID、任务 ID、页面或状态标识符等。
  • 不需要用户交互的字段:这些数据通常是预先定义或从其他上下文中获得的,用户无需手动输入。

4. 表单提交数据

在表单提交时,隐藏的 taskId 将与其他可见字段(如任务名称 taskName)一起提交。提交的数据将如下所示:

{
  "taskName": "写作任务",
  "taskId": "12345"
}

5. 结论

隐藏输入字段是一种有效的方式,可以在不影响用户界面的情况下将额外的数据发送到服务器。这些字段通常用于提交与特定记录相关的 ID 或其他无需用户输入的参数。

079 隐藏输入 (1)

使用隐藏输入字段传递组织 ID

在构建动态应用时,隐藏输入字段可以用于传递诸如组织 ID 之类的值,而无需用户交互或看到这些值。下面是一个使用隐藏输入字段的示例。

1. 添加隐藏的输入字段

我们在表单中添加一个 org_id 的隐藏字段,值为 123。在实际应用中,这个值可能是动态的,基于应用程序的状态。

<form>
  <label>
    用户名:
    <input type="text" name="username" />
  </label>

  <input type="hidden" name="org_id" value="123" />

  <button type="submit">提交</button>
</form>

2. 表单提交数据

当表单提交时,org_id 隐藏字段的数据将与其他表单字段一起发送到服务器,尽管用户看不到它。在提交请求时,数据将如下所示:

{
  "username": "example_user",
  "org_id": "123"
}

3. 检查隐藏输入

在浏览器的开发者工具中,虽然用户看不到隐藏输入,但你仍然可以在 DOM 中看到它:

<input type="hidden" name="org_id" value="123">

4. 动态设置值

如果应用需要动态设置 org_id,你可以在 JavaScript 中更改这个隐藏字段的值。例如:

document.querySelector('input[name="org_id"]').value = '456';

5. 用例

隐藏输入字段的常见使用场景:

  • 组织 ID:当提交表单时,附加组织或用户的上下文信息。
  • 会话 ID 或 CSRF 令牌:安全性相关的隐藏数据,可以随着表单提交。

总结:隐藏输入字段是一种强大且灵活的方式,用于提交用户看不到但需要传递到服务器的额外数据。

080 默认值

React 中的默认值 (Default Values)

在 React 中处理表单时,了解如何使用默认值来初始化表单元素非常重要。和传统的 HTML 一样,表单元素可以使用 valuechecked 等属性。但由于 React 强调动态性,使用这些属性的方式有所不同。

1. value vs defaultValue

在 HTML 中,你可以通过 value 属性来设置输入框的初始值。但在 React 中,如果直接使用 value 属性,这个输入框将变为“受控组件”,React 将完全控制其值。这意味着如果你使用 value,用户无法更改输入框的内容,除非你在代码中明确允许。

示例:使用 value

<input type="text" value="Kent C. Dodds" />

在这个例子中,输入框的值始终是 "Kent C. Dodds",用户无法编辑它。

2. 使用 defaultValue

如果你想初始化输入框的值,但允许用户修改它,则需要使用 defaultValue。这是 React 的特有属性,HTML 中没有这个属性。

示例:使用 defaultValue

<input type="text" defaultValue="Kent C. Dodds" />

在这个例子中,输入框的初始值是 "Kent C. Dodds",但用户可以自由修改。

3. 适用于不同类型的输入

  • 文本输入 (<input type="text">):可以使用 defaultValue
  • 复选框和单选按钮 (<input type="checkbox"><input type="radio">):可以使用 defaultChecked,而不是 checked
  • 日期 (<input type="date">)defaultValue 的格式需要符合日期格式(如 YYYY-MM-DD)。
  • 文件 (<input type="file">):文件输入不能有默认值。

4. 示例代码

<form>
  <label>
    用户名:
    <input type="text" defaultValue="Kent C. Dodds" />
  </label>

  <label>
    年龄:
    <input type="number" defaultValue={30} />
  </label>

  <label>
    公共账户:
    <input type="checkbox" defaultChecked={true} />
  </label>

  <label>
    开始日期:
    <input type="date" defaultValue="2024-10-23" />
  </label>

  <button type="submit">提交</button>
</form>

5. 注意事项

  • defaultValue 只在第一次渲染时有效:之后如果你修改它,React 不会自动更新 UI。
  • 复选框和单选按钮使用 defaultChecked:而不是 defaultValue
  • 日期的格式:日期输入需要使用 YYYY-MM-DD 的字符串格式,确保浏览器可以正确解析。

总结:在 React 中,使用 defaultValuedefaultChecked 可以设置表单元素的初始值,同时允许用户修改这些值。这与直接使用 valuechecked 不同,后者将使表单元素成为受控组件。

081 默认值 (1)

React 表单中的默认值

在这个视频中,我们讨论了如何在 React 中为表单元素设置默认值 (defaultValuedefaultChecked),以便用户可以更改输入,同时保留动态性。这与直接使用 valuechecked 的不同之处在于,defaultValuedefaultChecked 允许用户更新输入,而不会被 React 完全控制。

1. defaultValuedefaultChecked 的使用

  • 文本输入: defaultValue 用于初始化输入框的值,例如账号类型的默认值设为 student
  • 复选框和单选按钮: defaultChecked 用来设置默认选择项,例如将隐私设置默认设为公开 (public)。

2. 为不同类型的输入设置默认值

  • 数字输入 (<input type="number">): 你可以设置默认值为特定年龄,例如默认 18 岁。
  • 颜色选择器 (<input type="color">): 可以设置默认颜色,比如学校的代表色。
  • 日期选择器 (<input type="date">): 日期输入的默认值必须采用 YYYY-MM-DD 格式,你可以动态生成今天的日期。
  • 复选框 (<input type="checkbox">): 可以使用 defaultChecked 设置默认勾选状态。

3. 示例代码

<form>
  <label>
    账号类型:
    <select defaultValue="student">
      <option value="admin">管理员</option>
      <option value="teacher">教师</option>
      <option value="parent">家长</option>
      <option value="student">学生</option>
    </select>
  </label>

  <label>
    年龄:
    <input type="number" defaultValue={18} min={0} max={200} />
  </label>

  <label>
    默认颜色:
    <input type="color" defaultValue="#0033A0" />
  </label>

  <label>
    开始日期:
    <input type="date" defaultValue={new Date().toISOString().slice(0, 10)} />
  </label>

  <label>
    公开账户:
    <input type="radio" name="visibility" value="public" defaultChecked />
  </label>

  <label>
    私密账户:
    <input type="radio" name="visibility" value="private" />
  </label>

  <label>
    签署协议:
    <input type="checkbox" defaultChecked />
  </label>

  <button type="submit">提交</button>
</form>

4. 动态默认日期

如果你需要为日期输入动态生成默认值(如当天日期),你可以使用 new Date().toISOString().slice(0, 10) 获取 YYYY-MM-DD 格式的日期。

5. 注意事项

  • defaultValuedefaultChecked 只在初次渲染时生效。之后如果需要更新它们的值,你需要使用受控组件的方式。
  • 日期的默认值需要确保格式正确,否则浏览器可能无法正确解析。

通过使用 defaultValuedefaultChecked,你可以在 React 中灵活设置表单元素的初始值,同时保持用户可以修改这些值的自由。

082 爸爸笑话时间 输入

083 错误边界简介

React 错误边界 (Error Boundaries)

在这个课程中,我们将重点学习 错误边界(Error Boundaries),了解如何在 React 应用中优雅地处理错误。当应用发生错误时,错误边界可以防止用户看到空白页面,转而显示友好的错误提示或备用内容,从而提升用户体验。

什么是错误边界?

  • 错误边界是 React 组件,它们可以捕获子组件树中的 JavaScript 错误,记录错误信息,并渲染备用的 UI,而不是显示崩溃的组件树。
  • 注意,错误边界 无法 捕获事件处理器中的错误、异步代码中的错误(如 setTimeout)、服务端渲染的错误,或者 React 内部的错误。

关键概念:

  1. 使用类组件创建错误边界

    • 目前,错误边界必须通过 类组件 实现。类组件中可以使用 componentDidCatch()getDerivedStateFromError() 方法来处理错误。
  2. React Error Boundary 包

    • 与其手动编写类组件来创建错误边界,更推荐使用 React Error Boundary 包。这个第三方包能够简化在函数组件中处理错误的过程,被广泛应用于业界。你可以用它更具声明性地处理错误。
  3. 处理异步错误

    • 在异步操作(如 fetch 请求或 setTimeout)中发生的错误需要通过 try/catchpromise.catch 来处理。通过 React Error Boundary 提供的 useErrorBoundary() 钩子,可以在发生异步错误时触发错误边界。
  4. 多重错误边界

    • 就像 try/catch 块一样,你可以在应用中使用 嵌套错误边界。根据错误边界放置的位置,错误会被局部处理,这样可以为不同的 UI 部分提供更具体的错误提示或备用内容。

实践练习:

在这个练习中,你需要:

  • 在你的 React 应用中实现错误边界。
  • 使用 React Error Boundary 包来处理同步和异步的错误。
  • 确保你的应用在出现错误时能够优雅地恢复,并向用户显示适当的错误信息或备用内容,而不是空白页面。

通过使用错误边界,你可以增强应用的健壮性,在出错时为用户提供有意义的反馈,提升整体的用户体验。

084 组合

在这个练习中,我们要处理在 React 应用中出现的意外错误,比如无效的时间值错误。在实际开发中,用户体验非常重要,即使出现错误,我们也希望能优雅地处理,而不是让应用崩溃。为此,我们将使用 错误边界(Error Boundary) 来捕获这些错误并显示一个备用 UI。

目标:

  • 添加错误边界:我们将重构代码,把应用的主要部分包裹在一个错误边界内,用来处理不可预见的错误。
  • 显示错误信息:使用 fallback 组件来展示错误发生时的友好提示,而不是让整个页面崩溃或显示空白。

实现步骤:

  1. 安装 React Error Boundary

    • 我们使用 npm install react-error-boundary 来安装错误边界库,这是一个非常常用的第三方包,简化了 React 中错误处理的实现。
  2. 设置错误边界

    • 在你的根组件中,使用 ErrorBoundary 来包裹应用的其他部分。这样,当发生错误时,React 会渲染我们自定义的 fallback 组件。
  3. 添加 fallback 组件

    • 创建一个 ErrorFallback 组件,用来显示错误信息,并允许用户重新加载应用或继续操作。
  4. 处理错误

    • 使用错误边界捕获像无效时间格式这样的错误,并显示一个替代的 UI,提示用户发生了错误,并提供选项恢复。

示例代码:

import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>发生错误:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>重试</button>
    </div>
  );
}

function MyApp() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        // 重置逻辑,例如重载页面或重置状态
      }}
    >
      <YourMainApp />
    </ErrorBoundary>
  );
}

export default MyApp;

结果:

在这个实现中,ErrorBoundary 组件会捕获应用中可能出现的任何错误。无论是时间格式错误还是其他未处理的异常,应用都会显示我们定义的 ErrorFallback 组件,用户可以通过重试按钮重新加载应用。

通过这种方式,你不仅可以提升用户体验,还可以在生产环境中捕获更多的潜在错误,确保应用的健壮性。

085 组合 (1)

在这个练习中,我们使用 Error Boundary(错误边界)来处理 React 应用中出现的错误。通过这种方式,当应用中某个组件抛出错误时,我们可以优雅地捕获并向用户提供友好的错误提示,而不是让整个应用崩溃。

实现步骤:

  1. 引入 Error Boundary 和 Fallback Props:
    首先我们从 react-error-boundary 中引入 ErrorBoundary 组件和 FallbackProps 类型,用于处理错误和定义当错误发生时显示的组件。

  2. 重构代码以添加 Error Boundary:
    我们不能简单地在错误发生的地方添加 ErrorBoundary,因为在组件还没有完全创建之前,错误已经发生了。所以需要先将应用的主要部分封装为一个 onboardingForm 组件,然后在外部包裹 ErrorBoundary

  3. 创建错误回退组件
    我们定义了一个 ErrorFallback 组件,用于在发生错误时向用户展示错误信息。通过接收 FallbackProps,我们可以显示错误的具体信息,或者提供重试按钮,让用户重新加载页面。

  4. ErrorBoundary 使用
    使用 ErrorBoundary 包裹 onboardingForm,并传入 fallbackComponent 属性指定错误发生时显示的 UI。

示例代码:

import React from 'react';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
  return (
    <div role="alert">
      <p>发生错误:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>重试</button>
    </div>
  );
}

function OnboardingForm() {
  // 主应用代码
  return <div>欢迎来到我们的应用!</div>;
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        // 可选:在错误重置时执行的逻辑
      }}
    >
      <OnboardingForm />
    </ErrorBoundary>
  );
}

export default App;

关键点:

  • ErrorBoundary 会捕获它所包裹的子组件树中任何错误,渲染 FallbackComponent 来处理错误显示。
  • FallbackComponent 可以接受 errorresetErrorBoundary,允许我们根据错误类型自定义显示的信息,并为用户提供重试的机会。

通过这种方式,React 应用的用户体验大大提升。即使发生了错误,用户也不会看到空白页面或崩溃的应用,而是能获得有用的信息或选择重新尝试。

086 其他错误

在这个练习中,我们将探讨如何处理在 React 应用中的 异步错误。通常,try-catch 结构在同步代码中非常有效,但当代码涉及到异步操作或事件处理时,错误可能不会被 try-catch 捕捉到。

问题背景

当我们在编写代码时,如果某些操作(例如事件处理器或异步回调函数)发生错误,try-catch 无法捕获这些错误。这是因为这些操作运行在不同的上下文中。React 也有类似的限制:它能够捕获同步渲染中的错误,但不能自动捕获异步错误或事件处理中的错误。

例如,在以下场景中,当用户点击“提交”按钮时,如果代码中有错误(如调用 null 对象的方法),错误不会被 React 直接捕获,而是会抛出到全局上下文中:

function handleSubmit(event) {
  event.preventDefault();
  console.log(accountType.toUpperCase());  // 如果 accountType 为 null,就会抛出错误
}

任务目标

你需要确保这些类型的错误可以被捕获并且向用户提供有意义的错误信息,而不是让应用崩溃。

实现方法

  1. 使用 Error Boundaries 处理渲染错误
    对于渲染过程中发生的错误,我们可以使用 ErrorBoundary。然而,React 的 ErrorBoundary 无法处理 事件处理器异步代码 中的错误。

  2. 使用 Try-Catch 或 Promise Catch
    对于异步操作和事件处理器,我们需要手动捕获错误,通常使用 try-catch 或在 Promise 中使用 .catch

示例代码

让我们为 handleSubmit 函数添加一个 try-catch 语句来捕获可能发生的错误:

function handleSubmit(event) {
  event.preventDefault();
  
  try {
    // 可能抛出错误的代码
    console.log(accountType.toUpperCase());
  } catch (error) {
    console.error("捕获到错误:", error.message);
    alert("发生了错误,请稍后重试。");
  }
}

步骤

  1. 捕获事件处理中的错误
    在事件处理函数中包裹 try-catch,这样我们就能捕获到任何可能抛出的错误并为用户显示有意义的错误信息。

  2. 处理异步操作中的错误
    对于异步操作,我们可以在 .catch() 方法中处理错误。例如:

    async function fetchData() {
      try {
        const response = await fetch('/api/data');
        const data = await response.json();
        console.log(data);
      } catch (error) {
        console.error("异步操作错误:", error);
        alert("获取数据时发生了错误,请稍后重试。");
      }
    }

通过这些方法,我们可以确保 React 应用在处理异步操作或事件处理器中的错误时,仍然能提供良好的用户体验,避免应用崩溃或显示不友好的错误页面。

087 其他错误 (1)

在这个练习中,我们重点学习了如何处理异步操作或事件处理程序中抛出的错误,特别是当 React 的错误边界(Error Boundary)无法自动捕获这些错误时。我们使用 useErrorBoundary 钩子来手动抛出错误并显示错误边界。

问题背景

通常,React 只能在渲染过程中捕获同步错误。但在某些情况下,例如事件处理器或异步回调,错误是在不同的执行上下文中抛出的,React 无法自动捕获这些错误。这时,我们可以使用 try-catch 来捕获同步错误,或者在 Promise 中使用 .catch() 来处理异步错误。

实现方法

在这个例子中,我们使用了 useErrorBoundary 钩子,该钩子为我们提供了一个 showBoundary 函数。这个函数可以手动触发错误边界,显示给用户预定义的错误消息。

示例代码

以下是如何在提交表单时捕获错误并使用 showBoundary 触发错误边界的示例代码:

import { useErrorBoundary } from 'react-error-boundary';

function handleSubmit(event) {
  event.preventDefault();

  try {
    // 模拟一个可能抛出错误的操作
    console.log(accountType.toUpperCase());
  } catch (error) {
    // 如果发生错误,使用 showBoundary 显示错误边界
    showBoundary(error);
  }
}

function MyComponent() {
  const { showBoundary } = useErrorBoundary();

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">提交</button>
    </form>
  );
}

步骤

  1. 捕获同步错误:在事件处理函数(如 handleSubmit)中,使用 try-catch 来捕获任何可能抛出的错误。
  2. 手动触发错误边界:在 catch 块中,调用 showBoundary(error),这会触发 React 的错误边界并显示错误消息。
  3. 使用异步操作的 .catch():对于异步操作,您可以使用 .catch() 来处理 Promise 的错误,并同样使用 showBoundary 触发错误边界。

总结

通过使用 useErrorBoundary 钩子,我们能够在 React 无法自动捕获的情况下手动触发错误边界,为用户提供更好的错误处理体验。这种方法可以确保即使在异步操作或事件处理程序中发生错误,应用也能优雅地处理这些错误并向用户显示有意义的反馈。

088 重置

在这个练习中,我们学习了如何使用 resetErrorBoundary 方法,让用户在遇到错误后可以尝试重新执行操作。例如,网络连接问题可能导致错误,用户重新连接后可能希望再次尝试提交表单或进行其他操作。

关键点

  • resetErrorBoundary:这个方法是作为 error fallback 组件的一个 prop 提供的,允许用户在遇到错误后重置错误边界。
  • 状态重置:调用 resetErrorBoundary 后,错误边界会重置组件的状态,就像页面或组件第一次加载时的状态。这意味着之前的状态不会保留,因此如果状态引发了错误,重置后组件将恢复到初始状态。

示例代码

假设你有一个错误边界和一个 error fallback 组件,用户可以点击按钮尝试重新执行操作:

import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

function MyComponent() {
  const handleSubmit = () => {
    // 模拟错误
    throw new Error("Oops, something went wrong!");
  };

  return (
    <div>
      <button onClick={handleSubmit}>Submit</button>
    </div>
  );
}

export default function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
    >
      <MyComponent />
    </ErrorBoundary>
  );
}

步骤

  1. ErrorFallback 中使用 resetErrorBoundary:当用户点击 "Try again" 按钮时,resetErrorBoundary 方法会被调用,重置错误边界。

  2. 错误重置的行为:组件将重新加载,恢复到最初的状态。如果用户的输入数据很重要(例如一个大的表单),你可能需要将数据存储在本地存储或其他持久化存储中,以便在重置后重新加载这些数据。

总结

通过使用 resetErrorBoundary,我们能够提供一种优雅的方式,允许用户在遇到错误时重试操作。这是处理网络问题、临时性错误等场景的常见方法。

089 重置 (1)

在这个练习中,我们添加了一个功能,让用户在遇到错误时能够点击“再次尝试”按钮,从而重置错误边界,而不需要刷新整个页面。这通过使用 resetErrorBoundary 方法来实现,提供了一个更加流畅的用户体验。

主要步骤

  1. 从 props 中获取 resetErrorBoundary:通过将 resetErrorBoundaryerror fallback 组件的 props 中解构出来,我们能够在需要时调用它来重置错误状态。

  2. 创建“再次尝试”按钮:我们为按钮添加了一个 onClick 事件处理程序,点击该按钮时会调用 resetErrorBoundary 函数来重置错误边界,重新渲染组件。

  3. 重置后重新加载组件:当用户点击“再次尝试”时,组件会重新挂载,所有的输入状态(如表单数据)都会被重置。因此,如果用户希望保留他们在表单中的输入数据,可能需要在更高层级保存状态,或者使用浏览器的本地存储等方法来保存数据。

示例代码

import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

function MyComponent() {
  const handleSubmit = () => {
    // 模拟错误
    throw new Error("Oops, something went wrong!");
  };

  return (
    <div>
      <button onClick={handleSubmit}>Submit</button>
    </div>
  );
}

export default function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <MyComponent />
    </ErrorBoundary>
  );
}

关键点

  • resetErrorBoundary:用户点击“再次尝试”按钮时,调用 resetErrorBoundary 方法,重置错误边界并重新加载组件。
  • 数据丢失:重置组件后,输入的状态不会被保留,因此需要在更高层级管理状态,或者使用本地存储来保存用户输入的数据,避免在错误后丢失。

总结

通过使用 resetErrorBoundary,我们为用户提供了一个机会,在遇到错误时能够轻松地再次尝试提交,而不需要刷新整个页面。这种方式提升了用户体验,尤其是在发生偶发错误时,如网络错误等。

090 爸爸笑话时间 错误边界

091 数组渲染简介

在这段讲解中,讲述了在 React 中渲染数组时需要使用 key 属性的原因,以及如何通过 key 来帮助 React 跟踪 DOM 元素的变化。

关键点总结

  1. 渲染数组

    • 在 React 中,我们经常会将数组映射到 JSX 元素,比如将数组中的项渲染为 li 列表项。
    • 如果直接渲染数组项而不为每个元素提供唯一的 key 属性,React 将无法有效跟踪 DOM 中每个元素的变化,从而无法优化渲染过程。
  2. 为什么需要 key 属性

    • 当数组中的元素顺序改变时(例如,交换两个元素的位置),React 需要知道哪个 DOM 节点需要被移动或者更新。key 属性帮助 React 唯一标识每个元素,从而准确地更新 DOM,而不是重新渲染整个列表。
    • 如果没有 key,React 无法正确处理动态变化的列表,并且可能导致不必要的 DOM 操作,或者出现错误的更新。
  3. 如何选择 key 属性

    • 通常,key 应该是一个唯一标识符,如数据库中的 id
    • 如果没有唯一的 id,可以使用数组中的内容作为 key,但这需要确保内容是唯一的。
    • 不建议使用数组的索引作为 key,除非数组的顺序永远不会改变。
  4. key 的用途不仅仅是渲染数组

    • key 还可以用于组件的重置和重新渲染。如果你为一个组件提供一个不同的 key,React 将销毁旧组件并创建一个新的组件实例。

示例代码

const items = ['One', 'Two', 'Three'];

function List() {
  return (
    <ul>
      {items.map(item => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
}

在这个例子中,我们为每个 li 元素添加了 key 属性,使用 item 本身作为唯一标识符。React 现在可以正确地跟踪这些元素的变化。

额外思考

  • 如果数组的顺序会动态变化,选择一个稳定且唯一的 key 是至关重要的,这样可以确保 React 只会对那些确实改变的元素进行更新,而不会重新渲染整个列表。
  • 当你在项目中使用 lint 工具时,React 相关的规则通常会提醒你在渲染数组时需要提供 key 属性,这有助于避免潜在的性能问题和错误。

希望通过这个练习,你能理解为什么在 React 中渲染数组时需要 key,以及如何正确使用 key 属性。

092 Key prop

在这个练习中,我们要解决的是 React 渲染列表时没有 key 属性导致的问题。

问题说明

当我们在 React 中使用数组来渲染 UI 元素(如列表)时,如果没有为每个列表项 (li) 提供唯一的 key,React 在处理列表项的动态变化时可能会做出错误的猜测。例如,当你删除或添加某个列表项时,React 可能会错误地更新现有的元素,而不是移除或添加新的元素,从而导致 UI 显示错误。

例如,当你删除列表中的第一个元素时,React 可能会认为你只是修改了第一个元素的文本,而不会删除它并重新渲染。这样的话,列表的状态(例如输入框的内容)会被错误地保留,导致不正确的结果。

解决方法

为了解决这个问题,我们需要给每个 li 元素添加一个唯一的 key 属性,通常可以使用列表项的唯一标识符(例如 id)作为 key。这样 React 在遇到列表项的增删或顺序变化时,能够正确地识别每个元素,确保更新和删除操作能够正确执行。

示例代码

假设我们有一个水果列表,通过点击按钮来删除某些水果:

import React, { useState } from 'react';

const fruits = [
  { id: 1, name: 'Apple' },
  { id: 2, name: 'Banana' },
  { id: 3, name: 'Orange' },
];

function FruitList() {
  const [fruitList, setFruitList] = useState(fruits);

  const removeFruit = (id) => {
    setFruitList(fruitList.filter(fruit => fruit.id !== id));
  };

  return (
    <ul>
      {fruitList.map(fruit => (
        <li key={fruit.id}>
          <input type="text" value={fruit.name} readOnly />
          <button onClick={() => removeFruit(fruit.id)}>Remove</button>
        </li>
      ))}
    </ul>
  );
}

export default FruitList;

在这个例子中,我们为每个 li 元素指定了 key={fruit.id},以确保 React 能够正确识别每个列表项。当你删除某个水果时,React 会使用 key 来正确更新 DOM,移除相应的 li 元素,并且不会错误地保留其他元素的状态。

总结

通过为每个列表项添加唯一的 key 属性,React 能够更智能地处理 DOM 的更新,从而避免一些因猜测不准而导致的 UI 错误。这个练习帮助你理解在 React 中管理动态数组时,使用 key 属性的重要性。

093 Key prop (1)

在这个片段中,我们讨论了如何使用 key 属性来帮助 React 更好地管理列表元素的更新。

主要内容

  • 当我们使用 map 渲染数组时,React 需要通过 key 属性来跟踪每个列表项的变化。
  • 如果没有提供唯一的 key,React 可能会错误地猜测哪些元素应该被更新或删除,导致 UI 不一致。
  • 通过为每个 li 元素添加一个唯一的 key 属性(例如 item.id),React 可以更精确地处理 DOM 更新,确保列表项能够正确添加、删除或更新。

示例说明

  • 如果我们为每个 li 提供唯一的 key(如 item.id),React 可以识别哪个元素被移除或新增,并相应地更新 DOM。
  • 反之,如果没有 key 属性,React 可能会保留现有的 li 元素,只更新其内容而不是删除或重新渲染该元素。

总结

key 属性是确保 React 正确更新和管理动态列表的关键工具。通过为列表中的每个项提供唯一标识,React 可以精确跟踪元素的变化,从而优化渲染过程并确保数据和界面保持一致。

094 焦点状态

在这个步骤中,我们深入探讨了 key 属性在 React 中的作用,特别是它对输入组件状态、焦点状态等的影响。

主要内容

  1. key 属性的影响:

    • key 影响 React 如何处理列表元素的 DOM 更新,特别是在重新排序或删除时,确保组件的状态(如输入内容)得以正确保留。
    • 如果 key 设置得当,React 能够精确地跟踪哪些元素被添加、删除或更新,从而维护输入框的状态或焦点。
  2. 使用索引 (index) 作为 key 的问题:

    • 虽然数组索引是唯一的,但使用索引作为 key 并不能解决所有问题,尤其是在列表项目动态变化时。
    • 当使用索引作为 key 时,可能会导致 React 在重新渲染时无法正确跟踪组件状态,导致 UI 行为与预期不符。例如,在列表项被重新排序或删除时,输入框的状态可能会混乱。
  3. 探索和体验:

    • 鼓励你通过在 UI 中选择文本并编辑内容,观察使用不当的 key 会对输入状态和焦点产生怎样的影响。
    • 通过实验,你会注意到没有正确 key 时,React 可能会不恰当地更新组件,而不是保留用户输入或焦点。

总结

通过这个练习,目的是让你理解 key 属性在 React 中的关键性作用。当正确使用 key 时,React 能够精确地管理列表元素及其状态,避免由于不正确的 key 设置引发的状态丢失或 UI 错乱的情况。

095 焦点状态 (1)

在这个视频片段中,重点讲解了在 React 中 key 属性的关键作用,特别是当你动态更新列表项时,它如何影响焦点状态、输入状态以及组件的渲染行为。

关键点

  1. 选择项的丢失:

    • 当你不使用 key 或使用不当的 key(例如数组索引)时,React 不能准确跟踪列表项的变化。导致当你选择某个项目后,因重新排序或列表变动,选中的项可能被错误地更新或丢失。
  2. React 的最佳猜测:

    • React 会根据列表变化尝试“猜测”元素的更新方式。如果没有适当的 key,React 可能会错误地认为某个列表项只是内容更新,而不是位置移动或项目被替换。这可能导致焦点状态丢失、输入框中的文本消失或组件状态重置。
  3. key 的重要性:

    • 正确使用 key 可以帮助 React 识别和跟踪列表中的每个元素,即使它们的位置发生变化。这样 React 可以保留焦点、输入值等状态,而不会导致重新渲染或不必要的状态丢失。
    • 例如,使用唯一的 id 作为 key 能让 React 准确地识别元素,即使元素顺序或内容发生变化,用户的选择、焦点和输入状态仍能得到保留。
  4. 常见错误:

    • 使用数组索引作为 key 可能导致和不使用 key 相同的问题,React 仍然无法准确跟踪元素,因为当列表发生动态变化时,索引本身可能无法唯一标识元素。

总结

使用 key 是确保 React 正确管理组件状态和渲染行为的关键。当你正确使用 key,尤其是在列表或动态 UI 中,React 能够高效地更新 DOM,避免状态丢失,并提供更好的用户体验。

096 Key重置

在这个片段中,重点介绍了 key 属性的另一个应用场景,即在 React 中强制移除并重新加载某个组件或元素,以达到重置其状态的目的。

关键点

  1. key 强制更新组件:

    • key 属性发生变化时,React 会认为这个元素或组件是一个全新的实例。所有与之相关的状态、事件处理程序和 DOM 节点都会被移除,React 会重新渲染该元素。这可以用来强制重置组件的状态。
  2. 实际使用场景:

    • 通常你不会频繁地使用这种技巧,但在某些场景下非常有用。例如,当你希望用户能够通过点击按钮来重置输入框或表单的内容时,可以通过改变该元素的 key 来实现。
    • 例如,在这个例子中,当用户在输入框中输入内容后,点击“重置”按钮后,React 会移除现有的输入框并加载一个全新的输入框,确保之前输入的内容被清空。
  3. 任务目标:

    • 你的任务是让点击“重置”按钮后,输入框的 key 发生变化,从而让 React 重新渲染一个新的输入框。这样用户的输入内容会被清空,实现重置效果。

实现方法

通过简单地在按钮点击事件中改变输入框或相关父组件的 key 属性,React 将识别为该元素已更新,从而移除原有元素并重新渲染新的元素。

这个小技巧可以在需要清空组件状态或重置 UI 组件的场景下非常实用,尤其是在复杂的表单或交互中。

097 Key重置 (1)

在这个片段中,主要讲解了如何通过 key 属性重置 React 组件的状态。

关键点

  1. useState 状态管理:

    • 使用 useState 初始化了一个值(例如 0),并将其作为 input 元素的 key 值。当点击 "reset" 按钮时,通过更新这个 key 的值(将其加 1)来触发 React 认为这是一个全新的组件。
    • key 发生变化时,React 会移除旧的 input 元素并插入一个新的,而所有与旧组件相关的状态(如用户输入的内容)将会被重置。
  2. 点击按钮后的行为:

    • 当你在输入框中输入内容并点击 “reset” 按钮时,输入框的 key 值更新,这会告诉 React 重新渲染该 input 元素,完全移除先前的输入状态,使得组件呈现为初始状态。
  3. 应用场景:

    • 这种技术并不是经常使用的,但在某些需要重置用户界面的场景下非常有用。例如,重置一个表单的某部分,或者在某些交互操作失败时,允许用户重新开始。

总结

key 属性在 React 中不仅用于高效渲染列表元素,它也可以用来重置 UI 中某些组件的状态。当你需要移除某个组件的所有状态并重新开始时,通过更新该组件的 key 属性可以实现这一点。

098 爸爸笑话时间 数组渲染

099 React基础结束

这是一个祝贺和总结的片段,感谢你完成了所有的React练习,并复习了你所学的内容:

回顾:

  • 从最基础的 Hello World 开始,到创建DOM节点的基本操作。
  • 介绍了 React.createElement,并深入学习了 JSX组件 的使用。
  • 探讨了 TypeScript 的相关知识。
  • 学习了如何在React中应用样式,包括创建漂亮的蓝色盒子和 Box 组件的组合。
  • 深入讲解了 表单 操作,如何处理用户输入和交互。
  • 涉及了 错误处理 和用户点击 重试按钮 的机制。
  • 了解了关于 key 属性的重要性,特别是在处理动态列表和组件的状态重置时的作用。

结论:

你在这个系列练习中学到了很多有价值的React技能,从最基础的知识到更复杂的功能。现在你掌握了使用React构建各种有趣项目的能力。继续探索和构建,期待看到你未来的精彩作品!

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Oct 21, 2024

2 React钩子

001 React钩子简介

这段内容介绍了即将进入的React Hooks的学习内容,通过一些实际的应用示例展示了React Hooks在创建动态和交互式应用中的作用。

主要内容:

  1. 动态交互:展示如何通过React Hooks实现动态应用,比如搜索、收藏文章、通过查询字符串保留状态等。

  2. 状态管理:学习如何管理应用中的状态,并通过用户交互更新界面。

  3. React生命周期:通过一个示例说明React的渲染和更新流程,理解初始渲染和用户交互后状态更新触发的重新渲染机制。

  4. 外部库的整合:结合第三方库来实现一些视觉效果,比如计时器和动画。

  5. React Hooks流程图:学习React Hooks的生命周期,理解在状态变化时,组件的渲染、更新等过程。

  6. Tic-Tac-Toe 实践:通过实践项目(井字棋游戏),复习所学内容并深入理解,如使用本地存储保存状态。

最终,这段内容激励你开始学习,并鼓励大家通过React Hooks构建更加灵活、动态的应用。

002 UI状态管理简介

这段内容详细介绍了React应用的生命周期及如何通过React Hooks来管理状态,特别是通过useState hook实现动态更新的功能。

主要内容:

  1. React生命周期:React应用的生命周期包括初始渲染和后续的用户交互。当用户与页面交互时,React通过更新状态(state)来决定哪些UI元素需要更新,保证界面能够动态响应用户行为。

  2. State(状态)管理:状态可以被定义为随着时间变化的数据。在初始渲染时,React根据组件中的状态生成DOM节点。随着用户的操作,状态发生变化,React通过比较旧UI和新UI的不同来更新DOM。

  3. 使用useState管理状态

    • useState hook接收一个初始值,并返回两个值:当前状态值和更新状态的函数。
    • 状态的更新会触发React重新渲染组件,并确保界面内容始终是最新的。
  4. React更新机制:当状态变化时,React会重新调用相关的组件函数,生成新的UI,并通过比较新旧UI元素来高效地更新DOM,仅更新需要变化的部分。

  5. useState示例:讲解了一个计数器按钮的例子,展示了如何通过useState管理按钮的计数状态,并根据用户点击事件更新计数值。

  6. 声明式UI更新:React通过声明式的方式管理UI更新,开发者只需定义在当前状态下UI应该如何显示,React会高效处理DOM更新,避免了手动操作DOM的复杂性。

最后,内容预告了即将开始的练习,将结合实际项目(如博客搜索)进一步理解这些概念。

003 useState

这段内容描述了一个练习任务,目标是实现一个能够实时搜索博客文章的React应用。当前UI已经构建好,但输入框还没有与状态同步,因此在用户输入搜索内容时没有任何响应。

任务要点:

  1. 实时搜索功能:在用户输入搜索关键词时,UI需要动态地显示与搜索内容匹配的博客文章。
  2. 状态管理:使用useState来管理用户输入的搜索内容,确保输入框的变化能够与应用的状态同步。
  3. 事件处理:通过在输入框上添加onChange事件来捕捉用户输入,每次输入变化时,更新搜索的状态。

任务步骤:

  1. 在输入框的onChange事件中调用更新状态的函数。
  2. 通过useState hook管理输入框的当前值,并将其与UI绑定。

开发者还可以通过一个额外的挑战来增强React组件和UI生成的实践:删除现有的代码并从头构建整个UI,练习如何从零构建React组件。

完成目标:

  • 实现实时的搜索输入功能,使得输入框的值与状态保持同步,用户输入内容时,搜索结果将自动更新。

004 useState (1)

这段内容介绍了如何在React应用中使用useState来管理状态,使应用可以动态更新UI。具体步骤如下:

  1. 初始化状态

    • 使用useState来创建一个状态变量query,初始值为空字符串。通过useState返回的第二个函数setQuery用于更新query的值。
  2. 更新UI状态

    • 添加onChange事件处理器来捕捉用户在搜索框中的输入。通过event.currentTarget.value获取输入框的当前值,并使用setQuery更新状态。
  3. 动态搜索

    • 当输入框的内容发生变化时,搜索结果会根据query状态的变化动态过滤和更新显示。
  4. 多次调用useState

    • 演示了useState可以在一个组件中多次使用,React会根据调用顺序跟踪状态值。每次调用都会返回当前状态和更新该状态的函数。

总结

通过使用useState,你可以轻松地管理动态数据和UI状态,让应用在用户输入时做出实时响应。例如,在搜索框输入内容时,可以动态过滤和显示符合条件的博客文章。

这种模式广泛适用于React应用的各种场景,使得开发者能够轻松创建动态和交互式的用户界面。

005 控制输入

这段视频内容讲解了受控输入(Controlled Input)的概念,以及如何在React中通过编程方式控制输入值。下面是关键点总结:

1. 受控输入的概念

  • 在受控输入中,输入框的值(value)由React组件的状态(state)管理,输入框显示的值始终与状态中的值保持同步。
  • 通过设置input元素的value属性为useState中的值,React组件完全控制输入框的值。

2. 问题警告

  • 如果你设置了value属性但没有处理onChange事件,React会发出警告。原因是,用户尝试输入时React并没有被告知如何更新状态,所以输入框的值不会改变。解决方法是添加onChange处理函数,以便在用户输入时更新状态。

3. 编程控制输入

  • 在某些情况下,你可能需要编程方式控制输入框的值,而不仅仅是让用户输入。在这种情况下,受控输入非常有用。例如,视频中的例子演示了根据用户输入动态替换特定字符(例如将"koala"替换为表情符号🐨)。

4. 例子:通过复选框控制输入

  • 在后续的练习中,你将使用受控输入,当用户勾选复选框时,将自动更新输入框的查询值(如 "dog"、"cat"、"caterpillar"等),实现动态的输入值控制。

总结:

受控输入是React中常见的模式,允许开发者精确控制输入框的值,并根据需要进行编程处理。在React应用中,特别是当你需要自动填充或根据其他用户交互动态更改输入框内容时,受控输入非常有用。

006 控制输入 (1)

这段内容详细介绍了如何在React中使用受控输入(controlled input)来确保复选框与查询输入框的同步更新。以下是关键步骤和要点的总结:

1. 目标

  • 用户勾选或取消复选框时,查询输入框的内容应动态更新,反映所选复选框的标签。
  • 保持复选框和输入框的同步,这意味着当用户点击复选框时,输入框的内容会相应地添加或删除复选框对应的标签。

2. 不受控输入的实现

  • 开始时,没有直接控制输入框的值。我们使用handleCheck函数来处理复选框的状态变化:
    const handleCheck = (tag, checked) => {
        if (checked) {
            setQuery(query + ' ' + tag);  // 勾选时,添加标签
        } else {
            setQuery(query.replace(tag, '').trim());  // 取消勾选时,移除标签
        }
    };
  • 在复选框的onChange事件中调用handleCheck来根据复选框的选择状态(checked)更新查询字符串。

3. 将输入框设置为受控输入

  • 要保持输入框的值与复选框操作同步,必须将输入框的value属性设置为React组件状态的值。通过添加:
    <input value={query} />
  • 这使得输入框的值总是与query的状态保持一致,并且当复选框更新时,输入框中的内容也会随之更新。

4. 如何使受控输入起作用

  • useState hook被用来管理查询字符串的状态:
    const [query, setQuery] = useState('');
  • 然后,使用onChange事件处理用户的输入,使得输入框中的每个字符的变化都会更新查询状态。

5. 受控输入的好处

  • 通过控制输入框的值,可以确保用户在勾选或取消复选框时,输入框的值是最新的、反映当前状态的。
  • 使用handleCheck函数确保复选框的选择状态能够正确地影响输入框的内容。

6. 改进与调整

  • 可以进一步改进,比如确保标签之间没有多余的空格或重复的标签。
  • handleCheck函数进行优化,比如添加trim()函数以确保最终的查询字符串没有不必要的空格。

总结:

在这个练习中,学会了如何使用受控输入来控制用户界面的输入行为,特别是在用户与复选框交互时如何动态更新输入框的值。这是React中处理复杂状态和用户交互的一个非常重要的模式。

007 推导状态

在这一段内容中,主要介绍了如何使用派生状态(derived state)来解决当前复选框与查询框同步的问题。当用户在输入框中手动输入标签时,复选框的选中状态没有同步更新,反之亦然,因此需要通过派生状态来确保复选框的状态与查询输入框的内容保持一致。

问题描述:

当前,复选框和输入框能够互相影响,但是如果手动在输入框中输入“dog”或“cat”,相应的复选框不会自动选中,这会导致用户体验的困惑。为了解决这个问题,我们需要根据查询字符串来动态派生复选框的选中状态。

解决思路:派生状态

派生状态的核心思想是根据现有的状态来计算新的状态,而不是独立管理多个状态。具体到这个场景,就是从查询输入框的内容派生出复选框的选中状态,而不是手动单独管理复选框的状态。

步骤:

  1. 处理复选框的状态:
    我们需要在每次渲染UI时,根据当前查询字符串动态确定复选框的选中状态。例如,如果查询中包含“dog”,那么对应的“dog”复选框应该被选中。

  2. 实现派生状态:

    • 假设我们有多个复选框对应不同的标签(如dog、cat、caterpillar等),我们可以通过检查查询字符串中是否包含这些标签来派生它们的选中状态。
    • 举例:
      const isDogChecked = query.includes('dog');
      const isCatChecked = query.includes('cat');
      const isCaterpillarChecked = query.includes('caterpillar');
  3. 控制复选框状态:

    • 利用派生状态控制复选框的checked属性:
      <input type="checkbox" checked={isDogChecked} onChange={...} />
      <input type="checkbox" checked={isCatChecked} onChange={...} />
      <input type="checkbox" checked={isCaterpillarChecked} onChange={...} />
    • 这样,当用户在输入框中输入某个标签时,复选框的状态会自动根据输入框的内容进行更新。
  4. 同步逻辑的改进:

    • 当用户输入或删除查询中的某个标签时,不仅要更新查询输入框的内容,还要让复选框状态保持同步,确保每次渲染时复选框的状态反映查询字符串的当前内容。

代码示例:

const [query, setQuery] = useState('');

// 根据查询字符串派生复选框的选中状态
const isDogChecked = query.includes('dog');
const isCatChecked = query.includes('cat');
const isCaterpillarChecked = query.includes('caterpillar');

// 处理复选框的点击事件
const handleCheck = (tag, checked) => {
    if (checked) {
        setQuery(query + ' ' + tag);
    } else {
        setQuery(query.replace(tag, '').trim());
    }
};

// 渲染复选框并根据查询字符串设置复选框状态
return (
    <div>
        <input type="checkbox" checked={isDogChecked} onChange={(e) => handleCheck('dog', e.target.checked)} /> Dog
        <input type="checkbox" checked={isCatChecked} onChange={(e) => handleCheck('cat', e.target.checked)} /> Cat
        <input type="checkbox" checked={isCaterpillarChecked} onChange={(e) => handleCheck('caterpillar', e.target.checked)} /> Caterpillar
        <input type="text" value={query} onChange={(e) => setQuery(e.target.value)} />
    </div>
);

关键点:

  • 避免冗余状态: 不需要单独管理复选框的状态,只需要根据查询字符串的内容动态派生选中状态即可,这样能够减少状态不一致的可能性。
  • 用户体验的改进: 通过派生状态,用户可以通过手动输入或者点击复选框来更新查询,两个操作会自动同步,提升用户体验。

总结:

通过派生状态,我们能够确保复选框的状态与查询输入框内容保持一致,无论用户是手动输入还是勾选复选框,都能看到预期的结果。这种方式避免了管理多个冗余状态,代码更加简洁且易于维护。

008 推导状态 (1)

这一部分讲解了如何通过派生状态(derived state)来解决复选框的状态与查询输入框的同步问题。派生状态的思路是在已有状态的基础上进行计算,以确保UI的不同部分保持同步。具体操作是将查询字符串拆分为单词数组,并根据这些单词数组动态确定复选框的选中状态。

实现步骤:

  1. 派生单词状态:

    • 我们通过将查询字符串用空格拆分为单词数组,派生出一个单词列表:
      const words = query.split(' ');
    • 这是最基础的派生状态,将查询字符串转换为单词数组。
  2. 派生复选框的状态:

    • 基于派生的单词数组,检查是否包含某些特定的单词(如dogcatcaterpillar),从而派生出复选框的选中状态:
      const isDogChecked = words.includes('dog');
      const isCatChecked = words.includes('cat');
      const isCaterpillarChecked = words.includes('caterpillar');
  3. 控制复选框状态:

    • 通过checked属性控制复选框的状态,而不是value,因为checkbox的选中状态是通过checked属性来控制的:
      <input type="checkbox" checked={isDogChecked} onChange={...} /> Dog
      <input type="checkbox" checked={isCatChecked} onChange={...} /> Cat
      <input type="checkbox" checked={isCaterpillarChecked} onChange={...} /> Caterpillar
  4. 同步逻辑的改进:

    • 当用户手动输入标签时,复选框的状态会自动更新,反之亦然,确保每次渲染UI时,复选框的状态与查询字符串保持同步。

完整示例代码:

const [query, setQuery] = useState('');

// 派生单词数组
const words = query.split(' ');

// 派生复选框的状态
const isDogChecked = words.includes('dog');
const isCatChecked = words.includes('cat');
const isCaterpillarChecked = words.includes('caterpillar');

// 处理复选框点击事件
const handleCheck = (tag, checked) => {
    if (checked) {
        setQuery(prev => prev + ' ' + tag);
    } else {
        setQuery(prev => prev.replace(tag, '').trim());
    }
};

// 渲染复选框并根据查询字符串设置状态
return (
    <div>
        <input type="checkbox" checked={isDogChecked} onChange={(e) => handleCheck('dog', e.target.checked)} /> Dog
        <input type="checkbox" checked={isCatChecked} onChange={(e) => handleCheck('cat', e.target.checked)} /> Cat
        <input type="checkbox" checked={isCaterpillarChecked} onChange={(e) => handleCheck('caterpillar', e.target.checked)} /> Caterpillar
        <input type="text" value={query} onChange={(e) => setQuery(e.target.value)} />
    </div>
);

关键点:

  • 派生状态的优势: 使用派生状态可以减少冗余状态的管理,并确保不同部分的UI保持一致。通过派生状态,我们可以避免手动同步多个状态,简化代码逻辑。
  • 控制输入的实现: 通过将复选框的选中状态与查询字符串绑定,用户无论是通过输入还是点击复选框都能获得预期的结果。

总结:

派生状态是一种非常强大的方法,可以减少状态管理的复杂性。通过在当前状态的基础上动态生成新状态,可以确保应用程序的不同部分始终保持同步,从而提升用户体验和代码的可维护性。

009 初始化状态

这个部分讲解了如何在React应用中通过URL查询字符串来初始化应用的状态,使应用的状态可以被分享。当我们希望用户能够通过URL分享他们的查询内容(如:?query=cat+dog),页面在加载时应能够根据URL中的查询参数初始化UI。

实现步骤:

  1. 获取查询字符串:

    • 使用URLSearchParamswindow.location.search中获取查询参数,并从中提取出我们关心的query参数。
    • 如果查询参数不存在,使用空字符串作为默认值。
      const params = new URLSearchParams(window.location.search);
      const initialQuery = params.get('query') || '';
  2. 使用查询字符串初始化状态:

    • 将这个从URL获取的initialQuery传递给useState,作为初始值。
      const [query, setQuery] = useState(initialQuery);
  3. 渲染UI:

    • 页面加载时,会根据查询字符串中的query来初始化搜索框和复选框的状态。
    • 例如,如果URL为?query=cat+dog,那么页面加载后,搜索框中会显示cat dog,并且相关的复选框会被选中。

完整示例代码:

import React, { useState } from 'react';

const App = () => {
  // 从URL中获取查询参数
  const params = new URLSearchParams(window.location.search);
  const initialQuery = params.get('query') || '';

  // 使用查询字符串初始化状态
  const [query, setQuery] = useState(initialQuery);

  // 派生状态
  const words = query.split(' ');
  const isDogChecked = words.includes('dog');
  const isCatChecked = words.includes('cat');
  const isCaterpillarChecked = words.includes('caterpillar');

  // 处理复选框的变化
  const handleCheck = (tag, checked) => {
    if (checked) {
      setQuery(prev => prev + ' ' + tag);
    } else {
      setQuery(prev => prev.replace(tag, '').trim());
    }
  };

  return (
    <div>
      <input
        type="checkbox"
        checked={isDogChecked}
        onChange={(e) => handleCheck('dog', e.target.checked)}
      /> Dog
      <input
        type="checkbox"
        checked={isCatChecked}
        onChange={(e) => handleCheck('cat', e.target.checked)}
      /> Cat
      <input
        type="checkbox"
        checked={isCaterpillarChecked}
        onChange={(e) => handleCheck('caterpillar', e.target.checked)}
      /> Caterpillar
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
    </div>
  );
};

export default App;

关键点:

  • URL查询字符串的使用: 通过URLSearchParams从URL中提取查询参数,以便在应用初始化时能够根据用户的分享链接来设置应用状态。
  • 状态初始化: 我们通过URL中的query参数初始化状态,使得页面在加载时可以自动填充输入框和复选框,确保应用状态与URL保持同步。
  • 用户体验提升: 这样用户可以通过分享URL来传递他们的查询状态,而其他用户点击链接时,能够看到与发起者相同的UI状态。

总结:

通过这种方式,React应用能够根据URL中的查询参数初始化状态,确保应用状态的可分享性。这对于创建动态且易于分享的用户界面非常有用。

010 初始化状态 (1)

这一段讲解了如何基于URL查询字符串(如?query=cat+dog)动态初始化React组件的状态。通过这样做,可以确保页面在加载时,UI根据URL中的参数展示正确的内容。

关键点:

  1. 获取查询参数:
    使用URLSearchParamswindow.location.search中获取query参数。这样就可以从URL中提取并使用查询字符串。

    const params = new URLSearchParams(window.location.search);
    const initialQuery = params.get('query') || '';
  2. 用查询参数初始化状态:
    通过useState将从URL查询字符串中提取的query作为初始状态值。这确保页面加载时能使用URL中的值初始化UI。

    const [query, setQuery] = useState(initialQuery);
  3. 实时更新UI:
    查询参数会直接影响页面的输入框和复选框。例如,?query=cat+dog 会自动选中"cat"和"dog"的复选框,且搜索框中会显示cat dog

实现示例:

import React, { useState } from 'react';

const App = () => {
  // 从URL中获取查询参数
  const params = new URLSearchParams(window.location.search);
  const initialQuery = params.get('query') || '';

  // 用查询字符串初始化状态
  const [query, setQuery] = useState(initialQuery);

  // 派生状态
  const words = query.split(' ');
  const isDogChecked = words.includes('dog');
  const isCatChecked = words.includes('cat');
  const isCaterpillarChecked = words.includes('caterpillar');

  // 处理复选框变化
  const handleCheck = (tag, checked) => {
    if (checked) {
      setQuery(prev => prev + ' ' + tag);
    } else {
      setQuery(prev => prev.replace(tag, '').trim());
    }
  };

  return (
    <div>
      <input
        type="checkbox"
        checked={isDogChecked}
        onChange={(e) => handleCheck('dog', e.target.checked)}
      /> Dog
      <input
        type="checkbox"
        checked={isCatChecked}
        onChange={(e) => handleCheck('cat', e.target.checked)}
      /> Cat
      <input
        type="checkbox"
        checked={isCaterpillarChecked}
        onChange={(e) => handleCheck('caterpillar', e.target.checked)}
      /> Caterpillar
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
    </div>
  );
};

export default App;

进一步优化:

  1. 使用Router:
    在实际应用中,不建议直接从window.location.search中提取参数。推荐使用路由库(如React Router)提供的查询参数钩子(如useSearchParams),这样可以更灵活地处理路由和状态同步。

  2. 服务器端渲染(SSR)支持:
    在服务端渲染的场景下,window对象不存在,因此在这种情况下要谨慎使用直接引用window.location的方式。路由库通常能更好地支持SSR。

总结:

通过这种方法,可以根据URL中的查询参数动态初始化组件状态,使得页面内容与URL保持同步,提供了方便的状态分享和持久化功能。这在构建动态、可分享的用户界面时非常有用。

011 初始化回调

这段讲解了如何优化 useState 的初始值,特别是当初始值的计算非常耗时时,可以通过传递一个函数来延迟计算,从而提高性能。

关键点:

  1. 问题背景:
    当你在 useState 中传递初始值时,该初始值只在组件的初次渲染时被使用。如果初始值的计算非常耗时(比如解析大型 JSON、计算复杂公式等),那么即使只在初次渲染时需要,后续渲染时仍会不必要地执行这一计算,从而影响性能。

  2. 解决方案:
    React 允许你传递一个函数作为 useState 的参数。这样,初始值只会在第一次渲染时计算,后续渲染将不会再重复执行该计算。

    语法示例:

    const [value, setValue] = useState(() => {
      // 只在初次渲染时执行的计算
      return calculateInitialValue();
    });
  3. 如何应用:
    例如,如果你在页面加载时从 URL 中获取查询参数,可以将这一逻辑放在一个函数中,让 React 在需要时调用,而不是每次渲染都执行这个操作。

示例代码:

import React, { useState } from 'react';

const App = () => {
  // 定义一个函数来获取初始查询参数
  const getInitialQuery = () => {
    const params = new URLSearchParams(window.location.search);
    return params.get('query') || '';
  };

  // 使用 useState,传递函数作为初始值参数
  const [query, setQuery] = useState(getInitialQuery);

  // 处理输入框的变化
  const handleInputChange = (event) => {
    setQuery(event.target.value);
  };

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleInputChange}
      />
    </div>
  );
};

export default App;

重点:

  • getInitialQuery 这个函数只会在初次渲染时被调用。
  • 如果页面重新渲染,不会再次计算初始查询参数,从而节省了不必要的计算开销。

优化提示:

虽然这种方式提高了性能,但如果初始计算非常复杂并影响页面的首次加载速度,应该考虑进一步优化该计算的性能或异步执行,以避免影响用户体验。

总结:

这种将初始值计算封装到函数中的方式,适用于需要进行复杂或耗时计算的场景,它能确保计算只在必要时进行,避免浪费性能。

012 初始化回调 (1)

在这个视频里,重点是如何通过将 useState 的初始值计算提取到一个单独的函数中,以实现更好的性能优化。这里展示了如何使用 React 的懒初始化功能,让初始值的计算只在首次渲染时执行,而不是在每次组件重新渲染时都执行。

核心思路:

  1. 创建 getQueryParam 函数:你可以将用于计算初始查询参数的逻辑封装到 getQueryParam 函数中,并在 useState 中作为懒初始化函数传递。
  2. 避免不必要的重复计算:如果你直接在 useState 中计算初始值,那么每次组件重新渲染时都会调用计算函数。而通过将计算逻辑封装为函数并传递给 useState,你可以确保它只在初次渲染时调用。

代码示例:

import React, { useState } from 'react';

const App = () => {
  // 创建一个懒初始化函数,只有在组件初次渲染时才调用
  const getQueryParam = () => {
    const params = new URLSearchParams(window.location.search);
    return params.get('query') || '';
  };

  // 使用懒初始化函数作为 useState 的参数
  const [query, setQuery] = useState(getQueryParam);

  // 处理输入框的变化
  const handleInputChange = (event) => {
    setQuery(event.target.value);
  };

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleInputChange}
      />
    </div>
  );
};

export default App;

重点:

  • 优化前: 如果直接计算初始值,那么每次组件重新渲染时都会重复执行该计算,造成性能浪费。
  • 优化后: 通过传递一个懒初始化函数,React 只会在初次渲染时调用该函数,后续渲染时不会再重复执行这部分逻辑。

实现效果:

  • 当你访问页面时,比如 URL 是 ?query=cat+dog,输入框将显示 cat dog
  • 之后,无论你如何改变查询内容,这个 getQueryParam 函数只在初次渲染时调用,不会再每次重新渲染时重复调用。

结论:

通过懒初始化,可以避免在每次组件重新渲染时进行不必要的计算,从而优化性能。这种技术特别适合在计算量大或较为复杂的场景中使用。

013 爸爸笑话时间 UI状态管理

014 副作用简介

好的,我们来深入了解一下 useEffect 钩子,并通过它来处理 React 应用中的副作用(side effects)。在 React 中,副作用通常是指那些不直接与渲染相关的操作,比如与外部 API 通信、订阅服务、或直接操作 DOM 等。

以下是 useEffect 的几个关键点:

  • useEffect 用于处理副作用,比如网络请求、订阅或手动操作 DOM 等操作。
  • 它接收一个回调函数,这个函数包含要执行的副作用逻辑,还可以返回一个清理函数用于清除副作用。
  • 第二个参数是依赖项数组,只有当依赖项发生变化时,useEffect 中的副作用才会重新执行。
  • 注意避免副作用导致的无限循环问题,尤其是当副作用中涉及状态变化时,可能会导致组件不断重新渲染。
  • 在开发模式下,React 的严格模式可能会让 useEffect 回调函数执行两次,以帮助捕获某些潜在的错误。

提供的示意图帮助我们了解 useEffect 在 React 生命周期中的位置。通常,副作用会在初次渲染完成后执行,在组件卸载或依赖项改变时会清理先前的副作用并重新执行。

在这个练习中,我们需要确保 URL 中的查询参数与 React 组件中的搜索状态保持同步。虽然在实际应用中通常由路由器处理这一任务,但通过自己实现可以帮助更好地理解副作用是如何工作的,以及如何与外部状态进行同步。

现在让我们开始使用 useEffect,改进博客搜索界面并确保 URL 和搜索状态保持一致。

015 useEffect

任务:使用 popstate 事件更新 React 状态

在这个练习中,我们需要处理用户通过浏览器的前进和后退按钮时,React 应用中的状态与 URL 不同步的问题。尽管浏览器更新了 URL,我们的 React 应用程序并没有及时同步内部状态。

要实现这个同步,核心思路是在 React 应用中监听浏览器的 popstate 事件,当用户点击浏览器的前进或后退按钮时,根据 URL 的变化更新应用状态。我们将使用 useEffect 钩子来实现这一功能。

实现步骤

  1. 监听 popstate 事件

    • 在 React 的 useEffect 钩子中,添加一个监听器来监听 popstate 事件。
    • 当事件触发时,读取当前的 URL 参数并更新 React 的状态。
  2. 更新查询参数状态

    • popstate 事件处理程序中,我们将使用 URLSearchParams 来解析当前 URL 中的查询参数,并将其设置为组件的查询状态。
  3. 清理事件监听器

    • useEffect 钩子中添加清理函数,以确保组件卸载时能够正确移除事件监听器。

代码实现

import React, { useState, useEffect } from "react";
import { useLocation } from "react-router-dom"; // 如果你的项目中使用了react-router

function SearchComponent() {
  const [query, setQuery] = useState(""); // 查询参数的状态

  useEffect(() => {
    // 获取当前 URL 中的查询参数
    const getQueryFromUrl = () => {
      const params = new URLSearchParams(window.location.search);
      return params.get("query") || "";
    };

    // 初始化查询状态
    setQuery(getQueryFromUrl());

    // popstate 事件处理函数
    const handlePopState = () => {
      setQuery(getQueryFromUrl());
    };

    // 添加 popstate 事件监听器
    window.addEventListener("popstate", handlePopState);

    // 清理函数:组件卸载时移除事件监听器
    return () => {
      window.removeEventListener("popstate", handlePopState);
    };
  }, []);

  const handleSubmit = (event) => {
    event.preventDefault();
    const params = new URLSearchParams();
    params.set("query", query);
    window.history.pushState({}, "", `?${params.toString()}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search"
      />
      <button type="submit">Search</button>
    </form>
  );
}

export default SearchComponent;

说明:

  1. 初始化查询状态:一开始从当前 URL 中获取查询参数并初始化 query 状态。
  2. 监听 popstate 事件:当用户点击浏览器的前进或后退按钮时,popstate 事件会触发,更新应用的查询状态。
  3. 防止页面刷新:在表单提交时阻止页面刷新,并将查询参数更新到 URL 中。

通过这种方式,当用户点击浏览器的前进或后退按钮时,React 应用中的状态能够及时更新,实现 URL 和应用状态的同步。

016 useEffect (1)

使用 popstate 事件更新查询状态

在这个练习中,我们通过监听浏览器的 popstate 事件,使 React 应用的查询状态能够与浏览器的前进和后退操作同步更新。

实现步骤

  1. 监听 popstate 事件:使用 useEffect 钩子在组件挂载时添加事件监听器,监听 popstate 事件。
  2. 获取并设置查询参数:当 popstate 事件触发时,从 URL 中重新获取查询参数,并将其更新到组件的状态中。

代码实现

import React, { useState, useEffect } from "react";

function SearchComponent() {
  const getQueryParam = () => {
    const params = new URLSearchParams(window.location.search);
    return params.get("query") || "";
  };

  const [query, setQuery] = useState(getQueryParam);

  useEffect(() => {
    const handlePopState = () => {
      setQuery(getQueryParam);
    };

    // 添加 popstate 事件监听器
    window.addEventListener("popstate", handlePopState);

    // 清理函数,组件卸载时移除事件监听器
    return () => {
      window.removeEventListener("popstate", handlePopState);
    };
  }, []);

  const handleSubmit = (event) => {
    event.preventDefault();
    const params = new URLSearchParams();
    params.set("query", query);
    window.history.pushState({}, "", `?${params.toString()}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search"
      />
      <button type="submit">Search</button>
    </form>
  );
}

export default SearchComponent;

说明

  1. getQueryParam:这是一个函数,用于从当前 URL 中获取查询参数。
  2. useEffect:在组件挂载时,我们使用 window.addEventListener 监听 popstate 事件,当用户点击浏览器的前进或后退按钮时,更新 query 状态。
  3. 状态更新:每次 popstate 事件触发时,我们通过 getQueryParam 获取当前 URL 中的查询参数并更新组件的查询状态。
  4. 清理函数:当组件卸载时,我们会移除 popstate 事件监听器,避免内存泄漏。

结果

  • 当用户点击浏览器的前进或后退按钮时,React 应用会根据当前 URL 中的查询参数动态更新组件中的状态。

017 清理副作用

清理 useEffect 中的事件监听器

在 React 中,使用 useEffect 来设置事件监听器时,必须确保在组件卸载时清理这些监听器,否则会导致内存泄漏或其他潜在问题。

问题描述

我们当前的解决方案中,useEffect 设置了一个 popstate 事件监听器,但没有在组件卸载时正确地移除它。如果组件卸载后事件监听器还在运行,它可能会导致内存泄漏,保留不需要的变量,甚至会出现性能问题。

解决方案

通过在 useEffect 中返回一个清理函数来移除事件监听器。

代码实现

import React, { useState, useEffect } from "react";

function SearchComponent() {
  const getQueryParam = () => {
    const params = new URLSearchParams(window.location.search);
    return params.get("query") || "";
  };

  const [query, setQuery] = useState(getQueryParam);

  useEffect(() => {
    const handlePopState = () => {
      setQuery(getQueryParam());
    };

    // 添加 popstate 事件监听器
    window.addEventListener("popstate", handlePopState);

    // 返回一个清理函数,在组件卸载时移除事件监听器
    return () => {
      window.removeEventListener("popstate", handlePopState);
    };
  }, []);

  const handleSubmit = (event) => {
    event.preventDefault();
    const params = new URLSearchParams();
    params.set("query", query);
    window.history.pushState({}, "", `?${params.toString()}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search"
      />
      <button type="submit">Search</button>
    </form>
  );
}

export default SearchComponent;

说明

  1. useEffect 的清理函数:在 useEffect 中返回一个函数,该函数将在组件卸载时运行。在这个例子中,我们使用 window.removeEventListener 来移除 popstate 事件监听器。
  2. 防止内存泄漏:当组件被卸载时,清理函数会确保移除监听器,防止不必要的内存占用和潜在的性能问题。
  3. 模拟问题并测试:你可以通过在组件中反复显示和隐藏表单,并观察内存使用情况来验证内存泄漏问题是否得到解决。

小结

通过在 useEffect 中添加清理函数,可以避免由于事件监听器未被移除而引起的内存泄漏问题。这是 React 中处理副作用(如事件监听、订阅等)时非常重要的一个实践。

018 清理副作用 (1)

清理 useEffect 中的事件监听器的重要性

在 React 中,使用 useEffect 时,清理副作用(如事件监听器)是非常重要的。否则,可能会导致内存泄漏、性能下降,甚至会在组件卸载后继续执行无效操作。

问题演示

在之前的代码中,我们在 useEffect 中为 popstate 事件添加了监听器,但是没有返回清理函数。这样会导致内存泄漏问题,尤其是在处理大量数据时。例如,我们在代码中生成了包含一百万个元素的数组,未清理的事件监听器会持续占用内存,导致内存使用不断上升,严重时可能会使应用崩溃。

清理事件监听器的必要性

当我们没有移除事件监听器时,React 保留了对组件中的所有变量和函数的引用,即使组件已经卸载。这些未清理的引用会继续占用内存,并导致性能问题,尤其是在用户频繁操作页面时。

解决方案:添加清理函数

useEffect 中返回一个清理函数,确保组件卸载时正确移除事件监听器。

改进代码

import React, { useState, useEffect } from "react";

function SearchComponent() {
  const getQueryParam = () => {
    const params = new URLSearchParams(window.location.search);
    return params.get("query") || "";
  };

  const [query, setQuery] = useState(getQueryParam);

  useEffect(() => {
    const handlePopState = () => {
      setQuery(getQueryParam());
    };

    // 添加事件监听器
    window.addEventListener("popstate", handlePopState);

    // 返回清理函数,移除事件监听器
    return () => {
      console.log("Cleaning up event listener");
      window.removeEventListener("popstate", handlePopState);
    };
  }, []);

  const handleSubmit = (event) => {
    event.preventDefault();
    const params = new URLSearchParams();
    params.set("query", query);
    window.history.pushState({}, "", `?${params.toString()}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search"
      />
      <button type="submit">Search</button>
    </form>
  );
}

export default SearchComponent;

关键点

  1. 事件监听器的清理:在 useEffect 中,我们返回了一个函数,该函数将在组件卸载时被调用,用来移除 popstate 事件监听器。这样可以避免内存泄漏,并确保不会在组件卸载后继续调用无效的事件处理程序。

  2. 内存管理:通过正确清理副作用,确保即使在处理大量数据时,内存占用也能够被及时释放,从而避免性能问题。

  3. 性能影响:未清理的事件监听器可能导致组件卸载后仍在运行,继续占用内存和处理能力,特别是在移动设备等内存较少的环境中,影响更加明显。

小结

每当在 useEffect 中设置副作用时(如事件监听器、订阅等),确保返回一个清理函数,及时释放资源是 React 开发中的重要实践。这样不仅可以提升应用性能,还能避免潜在的内存泄漏问题。

019 爸爸笑话时间 副作用

020 状态提升简介

共享状态的挑战与提升状态的解决方案

很多人经常会遇到这样的问题:假设我们有一个组件结构,一个主应用组件(App),然后有多个嵌套的子组件,比如这个组件和那个组件。这些组件可能会形成复杂的层级结构。有时候,你会发现某个状态在一处被管理,但另一个较远的组件却需要访问这个状态。

这个时候,如何共享状态呢?解决方法就是提升状态,也就是将状态提升到最接近的共同父组件中进行管理。通过这种方式,状态可以被多个组件共享,并传递到那些需要访问它的子组件。

举个例子:

假设我们有一个应用程序(App),其中有一个计数器组件(Counter)来管理其状态。我们还想添加一个显示计数的组件(CountDisplay)。此时,状态在Counter组件中管理,我们希望将它传递给CountDisplay组件显示。

要做到这一点,首先我们需要将状态从Counter组件中提取出来,移动到App组件中。然后,App组件可以将这个状态以及更新状态的方法分别传递给Counter和CountDisplay组件,这样两个组件都可以访问和使用这个共享状态。

什么时候应该提升状态?

通常,React开发者已经非常熟悉提升状态的模式,并且在需要时知道如何进行状态提升。不过,当应用发生变化时,可能会遇到一个问题:我们是否需要把状态再次下移?比如,假设我们不再需要显示计数功能了,那么我们就可以删除CountDisplay组件,并将状态重新下移到Counter组件。

如果状态只被某个子组件使用,那么它应该被下移到该子组件中管理,避免状态在不相关的父组件中管理,从而提高代码的可读性和性能。

提升状态和本地化状态

因此,我们有两个概念:提升状态本地化状态。提升状态是为了让多个组件共享状态,而本地化状态则是当某个状态只在一个组件中使用时,将其下移到该组件中进行管理。

在接下来的练习中,我们将探讨这两个概念——提升状态和本地化状态,并通过实现一个点赞功能来加深理解。希望你已经准备好操作这个点赞功能,因为这次练习不仅仅是操作计数器,还涉及到如何管理一个收藏状态,比如一组被收藏的帖子 ID 等。

准备好了吗?让我们开始吧!

021 提升状态

修复收藏过滤体验并提升状态

在Kelly为我们重构代码以添加收藏功能时,导致了实际的过滤功能出现了问题。现在的任务是修复这个过滤功能,并且通过提升状态来实现这个修复。我们需要确保在操作收藏时,过滤功能仍能正常工作。

任务说明:

  1. 分析问题:

    • 收藏(favorite hearts)功能在代码重构过程中引入,但同时打破了过滤(filtering)的逻辑。
    • 这可能是由于状态在不合适的组件中被管理,导致无法在不同组件间有效共享和使用。
  2. 提升状态:

    • 将管理收藏状态(例如哪些帖子被收藏)的状态提升到能够影响过滤逻辑的公共父组件中。
    • 通过这种提升,父组件可以控制哪些帖子被过滤出来,以及被标记为收藏的帖子。
  3. 步骤概述:

    • 找出当前哪个组件在管理收藏状态,以及过滤逻辑在哪个组件中。
    • 将这些状态提取并提升到它们的共同父组件中(可能是最顶层的父组件,比如App组件),并在需要的组件间传递这个状态和相关的处理函数。
    • 确保过滤和收藏功能可以协同工作,保持代码结构简洁、易维护。

提示:

  • 提升状态时,请确保正确管理状态传递和更新机制。
  • 如果多个组件需要访问同一个状态,提升到最靠近的共同父组件会帮助我们更好地控制这些状态的变化。

让我们一起动手,修复过滤问题并提升状态吧!

022 提升状态 (1)

通过状态提升修复过滤功能

在这个步骤中,我们将通过提升状态来修复过滤功能的问题。当前状态(query)位于表单组件中,但多个组件都需要访问该状态。为了确保过滤功能和收藏功能都能正常工作,我们需要提升状态,将其放到一个共同的父组件中。

具体步骤:

  1. 找到需要提升的状态:

    • 过滤功能依赖于query,但这个query状态目前位于表单组件中,无法共享给其他组件。
    • 我们需要将querysetQuery提升到App组件中(或父组件),以便其他子组件也能访问和使用这个状态。
  2. 提升状态:

    • 将管理query的状态逻辑(包括useStatesetQuery)从表单组件中移动到父组件App中。
    • 这样,App可以将query状态作为prop传递给表单和其他需要它的子组件。
  3. 更新组件结构:

    • querysetQuery作为prop传递给Form组件,但仅传递queryMatchingPosts,因为MatchingPosts不需要修改状态,只需要读取状态。
    • 在表单中,确保更新了querysetQuery的使用,确保表单能够修改状态,并将状态变更反馈给父组件。
  4. 调整useEffect

    • 在某些情况下,状态管理逻辑需要放在更高的组件中,例如App。将与状态相关的useEffect逻辑也移到父组件中,这样可以确保所有逻辑集中处理。
  5. 验证结果:

    • 确保在表单中输入内容时,MatchingPosts组件能够正确过滤,并且在进行收藏等操作时,过滤功能仍然正常工作。

代码示例:

// App 组件
function App() {
  const [query, setQuery] = useState('');

  return (
    <div>
      <Form query={query} setQuery={setQuery} />
      <MatchingPosts query={query} />
    </div>
  );
}

// Form 组件
function Form({ query, setQuery }) {
  const handleInputChange = (event) => {
    setQuery(event.target.value);
  };

  return (
    <input
      type="text"
      value={query}
      onChange={handleInputChange}
      placeholder="Search posts"
    />
  );
}

// MatchingPosts 组件
function MatchingPosts({ query }) {
  // 过滤逻辑基于传入的 query 状态
  const filteredPosts = posts.filter(post => post.includes(query));

  return (
    <ul>
      {filteredPosts.map(post => <li key={post}>{post}</li>)}
    </ul>
  );
}

通过这种方式,所有需要访问query状态的组件都可以从父组件获得query,并且状态可以在不同的组件之间同步。

023 提升更多状态

提升状态以实现排序功能

在这个任务中,用户要求在显示帖子时,如果某个帖子被点赞(liked),那么它应该在列表中优先显示,随后再展示其他未被点赞的帖子。这意味着我们需要提升点赞状态(liked),将其提升到能够影响整个帖子列表排序的地方。

任务概述:

  • 目标:用户希望已被点赞的帖子在搜索结果中优先显示。
  • 挑战:当前点赞的状态被管理在每个帖子卡片组件内部。为了实现按点赞状态排序的功能,我们需要将这个状态提升到父组件,使父组件可以根据点赞状态进行重新排序。

具体步骤:

  1. 提升状态

    • 当前每个帖子的liked状态存储在它自己的组件中。为了实现排序逻辑,我们需要将liked状态提升到父组件(例如MatchingPosts),使它可以控制整个帖子列表的展示顺序。
  2. 传递状态和更新函数

    • MatchingPosts中管理点赞状态,并将状态和更新状态的函数作为prop传递给每个帖子的子组件。
    • 当用户点击点赞按钮时,将调用父组件的状态更新函数,从而改变liked状态,并影响帖子列表的排序。
  3. 实现排序逻辑

    • 在渲染帖子列表时,先按点赞状态排序,确保点赞的帖子排在最前面,然后再按其他排序规则(如时间、内容等)。

代码实现:

// App 组件
function App() {
  const [query, setQuery] = useState('');
  
  return (
    <div>
      <Form query={query} setQuery={setQuery} />
      <MatchingPosts query={query} />
    </div>
  );
}

// MatchingPosts 组件
function MatchingPosts({ query }) {
  const [likedPosts, setLikedPosts] = useState([]);

  const handleLikeToggle = (postId) => {
    setLikedPosts((prevLikedPosts) => {
      if (prevLikedPosts.includes(postId)) {
        return prevLikedPosts.filter(id => id !== postId);
      } else {
        return [...prevLikedPosts, postId];
      }
    });
  };

  // 模拟帖子数据
  const posts = [
    { id: 1, content: 'Dog post' },
    { id: 2, content: 'Cat post' },
    { id: 3, content: 'Caterpillar post' }
  ];

  // 过滤并按点赞状态排序
  const filteredPosts = posts
    .filter(post => post.content.includes(query))
    .sort((a, b) => likedPosts.includes(b.id) - likedPosts.includes(a.id));

  return (
    <ul>
      {filteredPosts.map(post => (
        <PostCard 
          key={post.id} 
          post={post} 
          isLiked={likedPosts.includes(post.id)} 
          onLikeToggle={() => handleLikeToggle(post.id)} 
        />
      ))}
    </ul>
  );
}

// PostCard 组件
function PostCard({ post, isLiked, onLikeToggle }) {
  return (
    <li>
      <span>{post.content}</span>
      <button onClick={onLikeToggle}>
        {isLiked ? 'Unlike' : 'Like'}
      </button>
    </li>
  );
}

// Form 组件
function Form({ query, setQuery }) {
  const handleInputChange = (event) => {
    setQuery(event.target.value);
  };

  return (
    <input
      type="text"
      value={query}
      onChange={handleInputChange}
      placeholder="Search posts"
    />
  );
}

解释:

  1. 状态提升:我们将每个帖子的liked状态提升到了MatchingPosts组件中,并在该组件中使用useState来管理所有帖子的点赞状态(使用帖子ID数组来记录哪些帖子被点赞)。

  2. 排序逻辑:在渲染帖子列表之前,我们根据likedPosts数组中的数据,对帖子进行排序。被点赞的帖子会先展示,未被点赞的帖子则在后面。

  3. 事件处理:在PostCard组件中,点击"Like"按钮会触发onLikeToggle,从而调用MatchingPosts中的handleLikeToggle函数来更新点赞状态。

这样实现了用户的需求,点赞的帖子将优先展示,并且可以正确处理用户的点赞操作。

024 提升更多状态 (1)

提升状态以实现按点赞排序功能

在这个任务中,我们遇到了一个稍微复杂的情况。用户希望当点击"喜欢"按钮时,帖子可以根据点赞状态排序,将已点赞的帖子排在最前面。这意味着我们不仅要提升isFavorited状态,还需要确保在点击时正确更新这个状态,并根据状态进行排序。

任务概述:

  • 目标:当用户点赞某个帖子时,该帖子会被移到列表的顶部。
  • 挑战:点赞状态目前存储在每个卡片组件中。为了实现全局的排序功能,我们需要将点赞状态提升到父组件,使得父组件可以根据点赞状态对帖子列表进行排序。

实现步骤:

  1. 提升状态

    • 我们首先将每个帖子的isFavorited状态从卡片组件提升到MatchingPosts组件中,并在父组件中用useState管理所有帖子的点赞状态。我们用帖子ID数组来记录哪些帖子已被点赞。
  2. 传递状态和更新函数

    • 父组件MatchingPosts需要传递isFavorited状态以及用于更新该状态的回调函数onFavoriteClick给每个子组件(即每个帖子卡片组件)。
  3. 实现排序逻辑

    • 我们根据点赞状态对帖子进行排序,确保被点赞的帖子排在最前面,然后再按其他排序规则(如发布时间等)排列未被点赞的帖子。

代码实现:

// MatchingPosts 组件
function MatchingPosts({ query }) {
  const [favorites, setFavorites] = useState([]);

  const handleFavoriteToggle = (postId) => {
    setFavorites((prevFavorites) => {
      if (prevFavorites.includes(postId)) {
        return prevFavorites.filter(id => id !== postId); // 取消点赞
      } else {
        return [...prevFavorites, postId]; // 点赞
      }
    });
  };

  // 模拟帖子数据
  const posts = [
    { id: 1, content: 'Dog post' },
    { id: 2, content: 'Cat post' },
    { id: 3, content: 'Caterpillar post' }
  ];

  // 过滤并按点赞状态排序
  const sortedPosts = posts
    .filter(post => post.content.includes(query))
    .sort((a, b) => favorites.includes(b.id) - favorites.includes(a.id));

  return (
    <ul>
      {sortedPosts.map(post => (
        <PostCard 
          key={post.id} 
          post={post} 
          isFavorited={favorites.includes(post.id)} 
          onFavoriteClick={() => handleFavoriteToggle(post.id)} 
        />
      ))}
    </ul>
  );
}

// PostCard 组件
function PostCard({ post, isFavorited, onFavoriteClick }) {
  return (
    <li>
      <span>{post.content}</span>
      <button onClick={onFavoriteClick}>
        {isFavorited ? 'Unlike' : 'Like'}
      </button>
    </li>
  );
}

解释:

  1. 状态提升:我们将每个帖子的isFavorited状态从卡片组件提升到MatchingPosts组件中,并使用useState来存储点赞的帖子ID数组。

  2. 排序逻辑:在渲染帖子列表之前,先根据favorites数组中的数据对帖子进行排序,点赞的帖子优先展示,未被点赞的帖子排在后面。

  3. 事件处理:在PostCard组件中,点击"Like"按钮会调用onFavoriteClick函数,该函数会触发handleFavoriteToggle,从而更新父组件中的点赞状态。

通过这种方式,我们实现了用户的需求,使得点赞的帖子会自动排到列表的最前面。

025 状态合并

任务概述

用户在使用点赞后将帖子自动移到列表顶部的功能时,觉得效果过于突兀,因此该功能已经被取消。现在需要将点赞状态的管理从父组件MatchingPosts中下放回到各个子组件(即帖子卡片组件),以简化代码并避免不必要的状态提升。

你的任务是将点赞状态(favorites数组)从父组件中移除,并在子组件中管理它,使状态只存在于需要的地方。这种做法称为状态的下放共定位(co-location),是为了让状态在只需要使用它的组件中维护,避免全局状态的复杂性。

任务步骤

  1. 删除父组件中的状态管理:我们需要从MatchingPosts组件中删除favorites和相关的状态管理逻辑。
  2. PostCard组件中管理isFavorited状态:点赞状态应只与每个帖子组件相关,因此应在PostCard组件中管理它。
  3. 简化组件通信:我们不再需要父组件传递onFavoriteClick等回调函数,因此可以简化MatchingPosts组件和PostCard组件之间的通信。

代码实现

1. 更新 MatchingPosts 组件:

// MatchingPosts 组件
function MatchingPosts({ query }) {
  // 模拟帖子数据
  const posts = [
    { id: 1, content: 'Dog post' },
    { id: 2, content: 'Cat post' },
    { id: 3, content: 'Caterpillar post' }
  ];

  // 过滤帖子
  const filteredPosts = posts.filter(post => post.content.includes(query));

  return (
    <ul>
      {filteredPosts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </ul>
  );
}

2. 更新 PostCard 组件:

// PostCard 组件
function PostCard({ post }) {
  const [isFavorited, setIsFavorited] = useState(false);

  const handleFavoriteClick = () => {
    setIsFavorited(!isFavorited); // 切换点赞状态
  };

  return (
    <li>
      <span>{post.content}</span>
      <button onClick={handleFavoriteClick}>
        {isFavorited ? 'Unlike' : 'Like'}
      </button>
    </li>
  );
}

解释:

  1. 删除全局状态:我们从MatchingPosts组件中删除了favorites数组及其状态管理,现在这个组件只负责渲染过滤后的帖子列表。

  2. 点赞状态共定位isFavorited状态现在只在PostCard组件中管理,不需要父组件参与。每个帖子都有自己独立的点赞状态。

  3. 简化组件通信:由于点赞状态的管理被下放到帖子组件中,父组件不再需要传递isFavoritedonFavoriteClick等回调,简化了组件之间的通信。

通过这个过程,我们将状态管理限定在需要的组件中,避免了不必要的状态提升,从而简化了代码结构并提高了可维护性。

026 状态合并 (1)

任务总结

我们已经完成了将点赞状态从父组件下放到子组件的过程,并简化了代码逻辑。通过共定位(co-location)状态,我们实现了以下几个关键点:

  1. 简化组件:不再需要将状态从父组件传递到子组件,减少了状态管理的复杂性。子组件独立管理其自身的状态,使代码更加模块化和易维护。

  2. 性能提升:通过在需要状态的组件内管理状态,减少了不必要的重渲染。只有实际需要更新的组件会重新渲染,从而提高了性能。

  3. 状态生命周期:共定位状态意味着当组件被卸载时,其状态也会随之消失。这是一把双刃剑,在某些场景下是合理的,但如果需要持久化状态(例如,用户点赞后状态不应丢失),则需要提升状态,甚至使用数据库来保存这些数据。

  4. 用户需求调整:尽管点赞功能最初需要将帖子移动到列表顶部,后期用户反馈认为这并不理想,因此我们移除了这一逻辑。但共定位状态的操作使代码结构更加清晰,为将来的扩展提供了良好的基础。

通过这次练习,你学会了如何在React中有效地管理状态,理解了状态提升状态共定位的概念,以及它们对组件性能和复杂度的影响。

后续步骤

你可以继续思考如何通过数据库或本地存储持久化这些状态,以及如何在全栈应用中实现更复杂的状态管理逻辑。

027 爸爸笑话时间 状态提升

028 DOM副作用简介

副作用与DOM操作

在之前的讨论中,我们已经介绍过副作用的概念。然而,有些副作用和其他的有所不同,尤其是当我们需要直接与DOM交互时,这就是我们在这个练习中要探讨的重点。

假设我们有一个很酷的库叫Vanilla Tilt,它为元素提供了一些有趣的动画效果。这是一个基于纯JavaScript编写的库,它不依赖于React的任何特性。因此,当我们想在React应用中集成它时,就需要找到一种方式来直接与DOM交互。

使用Ref来获取DOM节点

在React中,通常我们无法直接获取DOM节点,因为在调用React.createElement时,创建的仅仅是React元素,并不是DOM元素。真正的DOM元素只有在React完成渲染时才会被创建。因此,我们需要使用ref来获取已经渲染的DOM节点。

通过将ref属性应用在元素上,当React完成渲染后,ref属性会被调用并传递给我们DOM节点的引用。这就允许我们在DOM节点渲染完后执行一些操作,比如初始化第三方库或者添加一些原生的DOM事件监听。

<div ref={node => { /* 操作 node */ }}>

useRef Hook

除了直接在JSX中使用回调函数获取ref,React还提供了一个更为灵活的方式——useRef Hook。与useState类似,useRef返回一个可以在组件的整个生命周期中保持稳定的对象。这种对象包含一个current属性,我们可以通过它访问或更新引用的值。

不过,与useState不同的是,更新useRef不会引发组件的重新渲染。因此,在某些需要频繁更新的场景中,比如DOM节点的直接操作,useRef更加高效。

const myRef = useRef(null);

举例

const MyComponent = () => {
  const tiltRef = useRef(null);

  useEffect(() => {
    const tiltNode = tiltRef.current;
    // 初始化 Vanilla Tilt
    VanillaTilt.init(tiltNode);

    return () => {
      // 清理工作
      tiltNode.vanillaTilt.destroy();
    };
  }, []);

  return <div ref={tiltRef} className="tilt-element">Fancy Element</div>;
};

在上面的示例中,我们通过useRef获取到DOM节点,并使用useEffect在节点渲染完成后初始化第三方库,并在组件销毁时进行清理。这就是典型的副作用和DOM操作结合的场景。

总结

通过这个练习,我们探索了两种与DOM交互的方式:一种是通过ref回调函数,另一种是使用useRef Hook。了解什么时候使用哪种方式有助于我们编写更高效和易维护的代码。

029 Refs

使用Vanilla Tilt实现炫酷效果

现在我们的目标是通过Vanilla Tilt为一个元素添加炫酷的3D倾斜效果。我们已经准备好了一些代码,但需要你来处理ref部分。

我们将实现一个可见性切换按钮,确保你能够正确地进行清理操作。同时,我们还会在元素上实现点击计数器功能,这使得体验更加互动和有趣。

当你完成这个练习后,你应该能够看到:

  • 通过点击按钮可以切换元素的显示和隐藏。
  • 该元素带有Vanilla Tilt的3D倾斜效果。
  • 你可以点击该元素增加计数,并且效果保持一致。

步骤

  1. 添加ref: 你需要使用useRef获取元素的DOM节点。
  2. 初始化Vanilla Tilt: 在useEffect中初始化Vanilla Tilt,确保在组件渲染后绑定正确的DOM节点。
  3. 清理工作: 当元素被隐藏时,需要销毁Vanilla Tilt实例,避免内存泄漏。
import React, { useRef, useEffect, useState } from 'react';
import VanillaTilt from 'vanilla-tilt';

const TiltCard = () => {
  const [isVisible, setIsVisible] = useState(true);
  const [count, setCount] = useState(0);
  const tiltRef = useRef(null);

  useEffect(() => {
    if (tiltRef.current) {
      VanillaTilt.init(tiltRef.current, {
        max: 25,
        speed: 400,
        glare: true,
        "max-glare": 0.5,
      });

      return () => {
        // 清理操作
        tiltRef.current.vanillaTilt.destroy();
      };
    }
  }, [isVisible]);

  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>
        Toggle Visibility
      </button>
      
      {isVisible && (
        <div ref={tiltRef} className="tilt-element" onClick={() => setCount(count + 1)}>
          <h1>Count: {count}</h1>
        </div>
      )}
    </div>
  );
};

export default TiltCard;

关键点

  • useRef: 使用useRef来获取DOM节点,在元素渲染完成后绑定。
  • useEffect: 在useEffect中初始化Vanilla Tilt,并在组件卸载时销毁实例。
  • 清理: 确保在组件卸载时执行清理,防止内存泄漏。

完成之后,你应该会看到一个可交互的元素,带有炫酷的3D倾斜效果,并且能够在点击时增加计数。

030 Refs (1)

完成 Vanilla Tilt 的实现

我们现在已经成功通过 useRefVanilla Tilt 库互动,实现了一个 3D 倾斜效果,并且确保了内存的清理。我们还讨论了在 React 中如何通过 refs 与 DOM 进行交互,并保证了每次重新渲染时正确的清理工作。

总结一下:

  1. 使用 useRef: 我们用 useRef 来获取 DOM 元素的引用,并在 useEffect 中初始化 Vanilla Tilt。
  2. 处理清理: 我们通过返回一个清理函数确保在组件卸载时正确清理事件监听器,避免内存泄漏。
  3. 兼容性考虑: 我们添加了向后兼容的检查,处理可能返回 null 的情况。
  4. 日志输出: 我们利用 console.log 来观察 Vanilla Tilt 的初始化和清理过程,确保其行为符合预期。

以下是最终的代码:

import React, { useRef, useEffect, useState } from 'react';
import VanillaTilt from 'vanilla-tilt';

const TiltCard = () => {
  const [isVisible, setIsVisible] = useState(true);
  const [count, setCount] = useState(0);
  const tiltRef = useRef(null);

  useEffect(() => {
    if (tiltRef.current) {
      VanillaTilt.init(tiltRef.current, {
        max: 25,
        speed: 400,
        glare: true,
        "max-glare": 0.5,
      });

      return () => {
        // 清理操作,销毁 Vanilla Tilt 实例
        tiltRef.current.vanillaTilt.destroy();
      };
    }
  }, [isVisible]);

  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>
        Toggle Visibility
      </button>
      
      {isVisible && (
        <div ref={tiltRef} className="tilt-element" onClick={() => setCount(count + 1)}>
          <h1>Count: {count}</h1>
        </div>
      )}
    </div>
  );
};

export default TiltCard;

在这个练习中,你不仅学到了如何在 React 中处理 DOM 引用,还理解了如何通过 useEffectuseRef 来保证 DOM 操作的正确性和清理操作的及时性。这种模式非常适用于需要与第三方库互动的场景。

031 依赖项

控制 Vanilla Tilt 的优化

我们现在要优化之前的 Vanilla Tilt 实现,使其不在每次状态更新时重置效果,同时允许用户动态调整 tilt 的参数,例如调整速度和眩光等。

目标:

  1. 避免在每次组件重新渲染时重置 Vanilla Tilt 效果。
  2. 允许用户通过 UI 控制 Vanilla Tilt 的参数,如 max glarespeed
  3. 通过 useEffect 和依赖项优化 Vanilla Tilt 的初始化和清理过程。

代码实现:

import React, { useRef, useEffect, useState } from 'react';
import VanillaTilt from 'vanilla-tilt';

const TiltCard = () => {
  const [isVisible, setIsVisible] = useState(true);
  const [count, setCount] = useState(0);
  const [tiltOptions, setTiltOptions] = useState({
    max: 25,
    speed: 400,
    glare: true,
    "max-glare": 0.5,
  });
  const tiltRef = useRef(null);

  useEffect(() => {
    if (tiltRef.current) {
      // 初始化 Vanilla Tilt
      VanillaTilt.init(tiltRef.current, tiltOptions);

      return () => {
        // 清理 Vanilla Tilt 实例
        tiltRef.current.vanillaTilt.destroy();
      };
    }
  }, [tiltOptions]); // 依赖项为 tiltOptions,只有当选项改变时重新初始化

  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>
        Toggle Visibility
      </button>

      {isVisible && (
        <div>
          <div ref={tiltRef} className="tilt-element" onClick={() => setCount(count + 1)}>
            <h1>Count: {count}</h1>
          </div>

          {/* 控制选项 */}
          <div>
            <label>
              Speed: 
              <input
                type="range"
                min="100"
                max="1000"
                value={tiltOptions.speed}
                onChange={(e) => setTiltOptions({
                  ...tiltOptions,
                  speed: parseInt(e.target.value)
                })}
              />
            </label>

            <label>
              Max Glare: 
              <input
                type="range"
                min="0"
                max="1"
                step="0.1"
                value={tiltOptions["max-glare"]}
                onChange={(e) => setTiltOptions({
                  ...tiltOptions,
                  "max-glare": parseFloat(e.target.value)
                })}
              />
            </label>
          </div>
        </div>
      )}
    </div>
  );
};

export default TiltCard;

关键点:

  1. 使用 useEffect:我们将 Vanilla Tilt 的初始化逻辑移到了 useEffect 中,并将 tiltOptions 放入依赖数组。这样当用户调整参数时,Vanilla Tilt 会重新初始化,但不会在每次状态变更(如 count 变更)时重置。

  2. 控制参数的更新:我们为 max-glarespeed 提供了用户控制的 UI,使用 setTiltOptions 更新 tiltOptions,并通过 useEffect 自动触发重置。

接下来的问题:

尽管我们通过依赖项限制了重新初始化的次数,但由于 Vanilla Tilt 的 API 不支持直接更新配置,我们仍需要每次手动销毁旧实例并重新初始化,这可能影响体验。接下来我们可以考虑如何进一步优化这种场景。

通过这个实现,用户可以自由控制 tilt 的行为,并避免不必要的重新渲染带来的问题。

032 依赖项 (1)

使用 useRefuseEffect 结合处理 DOM 节点的状态管理

在这个部分,我们通过创建 useRef 来存储 DOM 节点的引用,并结合 useEffect 处理 Vanilla Tilt 的初始化和清理操作。最终目标是确保在每次用户更改配置时,能够正确地重新初始化 Vanilla Tilt,而不是在每次组件重新渲染时都重新初始化。

代码步骤和优化:

  1. 创建 useRef 引用
    使用 useRef 来存储 DOM 节点的引用。我们通过 tiltRef 来引用具体的 DOM 节点。

  2. 处理 useEffect
    Vanilla Tilt 的初始化逻辑放入 useEffect,并设置依赖项,当这些依赖项(如用户的 tiltOptions)改变时,触发重新初始化。

  3. 清理逻辑
    每次重新初始化之前,我们需要调用 Vanilla Tiltdestroy 方法来清理先前的事件监听器,避免内存泄漏。

代码实现:

import React, { useRef, useEffect, useState } from 'react';
import VanillaTilt from 'vanilla-tilt';

const TiltCard = () => {
  const [tiltOptions, setTiltOptions] = useState({
    max: 25,
    speed: 400,
    glare: true,
    "max-glare": 0.5,
  });
  const tiltRef = useRef(null);

  useEffect(() => {
    const tiltNode = tiltRef.current;

    if (tiltNode) {
      // 初始化 Vanilla Tilt
      VanillaTilt.init(tiltNode, tiltOptions);

      // 清理函数,防止内存泄漏
      return () => {
        tiltNode.vanillaTilt.destroy();
      };
    }
  }, [tiltOptions]); // 依赖项为 tiltOptions,确保只有当参数变化时重新初始化

  return (
    <div>
      {/* 控制选项 */}
      <div>
        <label>
          Speed: 
          <input
            type="range"
            min="100"
            max="1000"
            value={tiltOptions.speed}
            onChange={(e) => setTiltOptions({
              ...tiltOptions,
              speed: parseInt(e.target.value)
            })}
          />
        </label>

        <label>
          Max Glare: 
          <input
            type="range"
            min="0"
            max="1"
            step="0.1"
            value={tiltOptions["max-glare"]}
            onChange={(e) => setTiltOptions({
              ...tiltOptions,
              "max-glare": parseFloat(e.target.value)
            })}
          />
        </label>
      </div>

      {/* Vanilla Tilt DOM 元素 */}
      <div ref={tiltRef} className="tilt-element">
        <h1>Interactive Tilt Card</h1>
      </div>
    </div>
  );
};

export default TiltCard;

关键点:

  1. useRef:我们通过 useRef 创建了 tiltRef,用于存储 DOM 元素的引用。

  2. useEffect 与依赖项:使用 useEffect 监听 tiltOptions 的变化,当选项更新时,重新初始化 Vanilla Tilt,避免了不必要的多次初始化和销毁操作。

  3. 清理操作:在 useEffect 返回的函数中,我们使用 vanillaTilt.destroy() 清理所有的事件监听器,确保没有内存泄漏。

总结:

通过这种方式,我们确保了 Vanilla Tilt 只在用户改变参数时重新初始化,提升了应用的性能,同时避免了不必要的重新渲染和资源浪费。这种模式非常适用于在 React 中处理直接与 DOM 交互的第三方库。

033 原始依赖项

034 原始依赖项 (1)

035 爸爸笑话时间 DOM副作用

036 唯一ID简介

037 useId

038 useId (1)

039 爸爸笑话时间 唯一ID

040 井字棋简介

041 setState回调

042 setState回调 (1)

043 在localStorage中保存状态

044 在localStorage中保存状态 (1)

045 添加游戏历史功能

046 添加游戏历史功能 (1)

047 爸爸笑话时间 井字棋

048 React钩子结束

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Oct 21, 2024

3 高级React API

1 高级React API简介

2 高级状态管理简介

3 新状态_问题

4 新状态_解决方案

5 以前的状态_问题

6 以前的状态_解决方案

7 状态对象_问题

8 状态对象_解决方案

9 动作函数_问题

10 动作函数_解决方案

11 传统Reducer_问题

12 传统Reducer_解决方案

13 现实场景_问题

14 现实场景_解决方案

15 爸爸笑话时间 高级状态管理

16 状态优化简介

17 优化状态更新_问题

18 优化状态更新_解决方案

19 爸爸笑话时间 状态优化

20 自定义钩子简介

21 钩子函数_问题

22 钩子函数_解决方案

23 useCallback_问题

24 useCallback_解决方案

25 爸爸笑话时间 自定义钩子

26 共享上下文简介

27 上下文提供者_问题

28 上下文提供者_解决方案

29 上下文钩子_问题

30 上下文钩子_解决方案

31 爸爸笑话时间 共享上下文

32 门户简介

33 createPortal_问题

34 createPortal_解决方案

35 爸爸笑话时间 门户

36 布局计算简介

37 useLayoutEffect_问题

38 useLayoutEffect_解决方案

39 爸爸笑话时间 布局计算

40 命令式处理简介

41 useImperativeHandle_问题

42 useImperativeHandle_解决方案

43 爸爸笑话时间 命令式处理

44 焦点管理简介

45 flushSync_问题

46 flushSync_解决方案

47 爸爸笑话时间 焦点管理

48 同步外部状态简介

49 useSyncExternalStore_问题

50 useSyncExternalStore_解决方案

51 创建存储实用程序_问题

52 创建存储实用程序_解决方案

53 处理服务器渲染_问题

54 处理服务器渲染_解决方案

55 爸爸笑话时间 同步外部状态

56 高级React API结束

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Oct 21, 2024

4 React Suspense

1 数据获取简介

2 抛出Promise_问题

3 抛出Promise_解决方案

4 错误处理_问题

5 错误处理_解决方案

6 正式状态_问题

7 正式状态_解决方案

8 实用工具_问题

9 实用工具_解决方案

10 使用React_问题

11 使用React_解决方案

12 爸爸笑话时间 数据获取

13 动态Promise简介

14 Promise缓存_问题

15 Promise缓存_解决方案

16 useTransition_问题

17 useTransition_解决方案

18 挂起闪烁_问题

19 挂起闪烁_解决方案

20 爸爸笑话时间 动态Promise

21 乐观UI简介

22 乐观UI_问题

23 乐观UI_解决方案

24 表单状态_问题

25 表单状态_解决方案

26 多步操作_问题

27 多步操作_解决方案

28 爸爸笑话时间 乐观UI

29 Suspense图像组件简介

30 图像组件_问题

31 图像组件_解决方案

32 图像错误边界_问题

33 图像错误边界_解决方案

34 Key属性_问题

35 Key属性_解决方案

36 爸爸笑话时间 Suspense图像

37 响应式简介

38 useDeferredValue_问题

39 useDeferredValue_解决方案

40 爸爸笑话时间 响应式

41 优化简介

42 并行加载_问题

43 并行加载_解决方案

44 服务器缓存_问题

45 服务器缓存_解决方案

46 爸爸笑话时间 优化

47 React Suspense结束

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Oct 21, 2024

5 高级React模式

1 高级React模式简介

2 组合简介

3 组合和布局组件_问题

4 组合和布局组件_解决方案

5 爸爸笑话时间 组合

6 最新Ref简介

7 最新Ref_问题

8 最新Ref_解决方案

9 爸爸笑话时间 最新Ref

10 复合组件简介

11 复合组件_问题

12 复合组件_解决方案

13 复合组件验证_问题

14 复合组件验证_解决方案

15 爸爸笑话时间 复合组件

16 插槽简介

17 插槽上下文_问题

18 插槽上下文_解决方案

19 通用插槽组件_问题

20 通用插槽组件_解决方案

21 插槽Prop_问题

22 插槽Prop_解决方案

23 爸爸笑话时间 插槽

24 Prop集合和Getters简介

25 Prop集合_问题

26 Prop集合_解决方案

27 Prop Getters_问题

28 Prop Getters_解决方案

29 爸爸笑话时间 Prop集合和Getters

30 状态初始化器简介

31 初始化切换_问题

32 初始化切换_解决方案

33 稳定性_问题

34 稳定性_解决方案

35 爸爸笑话时间 状态初始化器

36 状态Reducer简介

37 状态Reducer_问题

38 状态Reducer_解决方案

39 默认状态Reducer_问题

40 默认状态Reducer_解决方案

41 爸爸笑话时间 状态Reducer

42 控制属性简介

43 控制属性_问题

44 控制属性_解决方案

45 爸爸笑话时间 控制属性

46 高级React模式结束

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Oct 21, 2024

6 React性能优化

1 React性能优化简介

2 元素优化简介

3 重用元素_问题

4 重用元素_解决方案

5 元素Props_问题

6 元素Props_解决方案

7 上下文_问题

8 上下文_解决方案

9 元素记忆化_问题

10 元素记忆化_解决方案

11 组件记忆化_问题

12 组件记忆化_解决方案

13 爸爸笑话时间 元素优化

14 优化上下文简介

15 上下文记忆化_问题

16 上下文记忆化_解决方案

17 提供者组件_问题

18 提供者组件_解决方案

19 分割上下文_问题

20 分割上下文_解决方案

21 爸爸笑话时间 优化上下文

22 并发渲染简介

23 useDeferredValue + memo_问题

24 useDeferredValue + memo_解决方案

25 爸爸笑话时间 并发渲染

26 代码拆分简介

27 lazy_问题

28 lazy_解决方案

29 预加载_问题

30 预加载_解决方案

31 过渡_问题

32 过渡_解决方案

33 爸爸笑话时间 代码拆分

34 高耗计算简介

35 useMemo_问题

36 useMemo_解决方案

37 Web Worker_问题

38 Web Worker_解决方案

39 异步结果_问题

40 异步结果_解决方案

41 爸爸笑话时间 高耗计算

42 渲染优化简介

43 组件记忆化_问题

44 组件记忆化_解决方案

45 自定义比较器_问题

46 自定义比较器_解决方案

47 原始类型_问题

48 原始类型_解决方案

49 爸爸笑话时间 渲染优化

50 窗口化简介

51 Virtualizer_问题

52 Virtualizer_解决方案

53 爸爸笑话时间 窗口化

54 React性能优化结束

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Oct 21, 2024

7 React服务器组件

1 React服务器组件简介

2 热身简介

3 静态React应用_问题

4 静态React应用_解决方案

5 爸爸笑话时间 热身

6 服务器组件简介

7 服务器组件_问题

8 异步组件_问题

9 异步组件_解决方案

10 流式传输_问题

11 流式传输_解决方案

12 服务器上下文_问题

13 服务器上下文_解决方案

14 爸爸笑话时间 服务器组件

15 客户端组件简介

16 Node.js加载器_问题

17 Node.js加载器_解决方案

18 模块解析_问题

19 模块解析_解决方案

20 爸爸笑话时间 客户端组件

21 客户端路由简介

22 客户端路由_问题

23 客户端路由_解决方案

24 挂起UI_问题

25 挂起UI_解决方案

26 竞争条件_问题

27 竞争条件_解决方案

28 历史_问题

29 历史_解决方案

30 缓存_问题

31 缓存_解决方案

32 爸爸笑话时间 客户端路由

33 服务器操作简介

34 操作引用_问题

35 操作引用_解决方案

36 客户端_问题

37 客户端_解决方案

38 服务器_问题

39 服务器_解决方案

40 重新验证_问题

41 重新验证_解决方案

42 历史重新验证_问题

43 历史重新验证_解决方案

44 爸爸笑话时间 服务器操作

45 React服务器组件结束

@WangShuXian6
Copy link
Owner Author

WangShuXian6 commented Oct 21, 2024

8 额外:专家访谈

1 与Aakansha Doshi讨论如何进入开源

2 Aurora Scharff讲解如何使用React 19增强表单

3 Jenna Smith谈AI、Radix构建和Tokenami

4 Evan Bacon将React服务器组件引入React Native

5 Kateryna Porshnieva讲解如何使用React 19构建无障碍应用

6 Lee Robinson讲述React的演变:过去、现在与未来

7 Matt Brophy谈Remix、React Router和开源

8 Michelle Beckles谈社区建设和开发者健康

9 Rick Hanlon讲解React 19的内幕

10 Sam Selikoff谈React在现代Web开发中的影响

11 Lydia Hallie讨论JavaScript、React和Web开发的未来

12 Sebastian Silbermann讲解React 19的测试、工具和过渡

13 Shruti Kapoor讲述现代Web开发中的无障碍性重要性

14 Sunil Pai谈如何通过强大的软件、PartyKit和耐久对象改变生活

15 Theo Browne谈他的个人Web开发经验

16 Dominik Dorfmeister谈他的开源之旅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant