React组件化练习

本文深入探讨React的高级特性,包括Context上下文管理、高阶组件(HOC)的实用案例,以及Hooks的状态管理和副作用处理。通过具体代码示例,讲解如何优雅地使用这些工具来提升组件间的通信效率和代码复用性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 
资源

Context参考

HOC参考

Hooks参考

组件跨层级通信 - Context

范例:模拟 redux 存放全局状态,在组件间共享
 
import React from 'react'
// 创建上下文
const Context = React.createContext()
// 获取Provider和Consumer
const Provider = Context.Provider
const Consumer = Context.Consumer
// Child显示计数器,并能修改它,多个Child之间需要共享数据
function Child(props) {
  return <div onClick={() => props.add()}>{props.counter}</div>
}
export default class ContextTest extends React.Component {
  // state是要传递的数据
  state = {
    counter: 0,
  }
  // add方法可以修改状态
  add = () => {
    this.setState((nextState) => ({ counter: nextState.counter + 1 }))
  }
  // counter状态变更
  render() {
    return (
      <Provider value={{ counter: this.state.counter, add: this.add }}>
        {/* Consumer中内嵌函数,其参数是传递的数据,返回要渲染的组件 */}
        {/* 把value展开传递给Child */}
        <Consumer>{(value) => <Child {...value} />}</Consumer>
        <Consumer>{(value) => <Child {...value} />}</Consumer>
      </Provider>
    )
  }
}

 

高阶组件

范例:为展示组件添加获取数据能力
// Hoc.js
import React from 'react'
// Lesson保证功能单一,它不关心数据来源,只负责显示
function Lesson(props) {
  return (
    <div>
      {props.stage} - {props.title}
    </div>
  )
}
// 模拟数据
const lessons = [
  { stage: 'React', title: '核心API' },
  { stage: 'React', title: '组件化1' },
  { stage: 'React', title: '组件化2' },
]
// 高阶组件withContent负责包装传入组件Comp
// 包装后组件能够根据传入索引获取课程数据,真实案例中可以通过api查询得到
const withContent = (Comp) => (props) => {
  const content = lessons[props.idx]
  // {...props}将属性展开传递下去
  return <Comp {...content} />
}
// LessonWithContent是包装后的组件
const LessonWithContent = withContent(Lesson)
export default function HocTest() {
  // HocTest渲染三个LessonWithContent组件
  return (
    <div>
      {[0, 0, 0].map((item, idx) => (
        <LessonWithContent idx={idx} key={idx} />
      ))}
    </div>
  )
}

 

范例:改造前面案例使上下文使用更优雅

链式调用

// withConsumer是高阶组件工厂,它能根据配置返回一个高阶组件
function withConsumer(Consumer) {
  return (Comp) => (props) => {
    return <Consumer>{(value) => <Comp {...value} {...props} />}</Consumer>
  }
}
// Child显示计数器,并能修改它,多个Child之间需要共享数据
// 新的Child是通过withConsumer(Consumer)返回的高阶组件包装所得
const Child = withConsumer(Consumer)(function (props) {
  return (
    <div onClick={() => props.add()} title={props.name}>
      {props.counter}
    </div>
  )
})
export default class ContextTest extends React.Component {
  render() {
    return (
      <Provider value={{ counter: this.state.counter, add: this.add }}>
        {/* 改造过的Child可以自动从Consumer获取值,直接用就好了 */}
        <Child name="foo" />
        <Child name="bar" />
      </Provider>
    )
  }
}
// 高阶组件withLog负责包装传入组件Comp
// 包装后组件在挂载时可以输出日志记录
const withLog = (Comp) => {
  // 返回组件需要生命周期,因此声明为class组件
  return class extends React.Component {
    render() {
      return <Comp {...this.props} />
    }
    componentDidMount() {
      console.log('didMount', this.props)
    }
  }
}
// LessonWithContent是包装后的组件
const LessonWithContent = withLog(withContent(Lesson))

 

装饰器写法

CRA 项目中默认不支持 js 代码使用装饰器语法,可修改后缀名为 tsx 则可以直接支持
注意修改 App.js 中引入部分,添加一个后缀名
要求 cra 版本高于 2.1.0

组件复合 - Composition

复合组件给与你足够的敏捷去定义自定义组件的外观和行为

组件复合

范例: Dialog 组件负责展示,内容从外部传入即可, components/Composition.js
// 装饰器只能用在class上
// 执行顺序从下往上
@withLog
@withContent
class Lesson2 extends React.Component {
  render() {
    return (
      <div>
        {this.props.stage} - {this.props.title}
      </div>
    )
  }
}
export default function HocTest() {
  // 这里使用Lesson2
  return (
    <div>
      {[0, 0, 0].map((item, idx) => (
        <Lesson2 idx={idx} key={idx} />
      ))}
    </div>
  )
}
import React from 'react'
// Dialog定义组件外观和行为
function Dialog(props) {
  return <div style={{ border: '1px solid blue' }}>{props.children}</div>
}
export default function Composition() {
  return (
    <div>
      {/* 传入显示内容 */}
      <Dialog>
        <h1>组件复合</h1>
        <p>复合组件给与你足够的敏捷去定义自定义组件的外观和行为</p>
      </Dialog>
    </div>
  )
}

 

范例:传个对象进去, key 表示具名插槽
import React from 'react'
// 获取相应部分内容展示在指定位置
function Dialog(props) {
  return (
    <div style={{ border: '1px solid blue' }}>
      {props.children.default}
      <div>{props.children.footer}</div>
    </div>
  )
}
export default function Composition() {
  return (
    <div>
      {/* 传入显示内容 */}
      <Dialog>
        {{
          default: (
            <>
              <h1>组件复合</h1>
              <p>复合组件给与你足够的敏捷去定义自定义组件的外观和行为</p>
            </>
          ),
          footer: <button onClick={() => alert('react确实好')}>确定</button>,
        }}
      </Dialog>
    </div>
  )
}

 

如果传入的是函数,还可以实现作用域插槽的功能
如果 props.children jsx ,此时它是不能修改的
范例:实现 RadioGroup Radio 组件,可通过 RadioGroup 设置 Radio name
function RadioGroup(props) {
  // 不可行,
  // React.Children.forEach(props.children, child => {
  // child.props.name = props.name;
  // });
  return (
    <div>
      {React.Children.map(props.children, (child) => {
        // 要修改child属性必须先克隆它
        return React.cloneElement(child, { name: props.name })
      })}
    </div>
  )
}

 

Hooks

准备工作

升级 react react-dom

状态钩子 State Hook

创建 HooksTest.js
 
// Radio传入value,name和children,注意区分
function Radio({ children, ...rest }) {
  return (
    <label>
      <input type="radio" {...rest} />
      {children}
    </label>
  )
}
export default function Composition() {
  return (
    <div>
      {/* 执行显示消息的key */}
      <RadioGroup name="mvvm">
        <Radio value="vue">vue</Radio>
        <Radio value="react">react</Radio>
        <Radio value="ng">angular</Radio>
      </RadioGroup>
    </div>
  )
}

 

npm i react react-dom -S

import React, { useState } from "react";
export default function HooksTest() {
// useState(initialState),接收初始状态,返回一个由状态和其更新函数组成的数组
const [fruit, setFruit] = useState("");
return (
<div>
<p>{fruit === "" ? "请选择喜爱的水果:" : `您的选择是:${fruit}`}</p>
</div>
);
}

 

声明多个状态变量
// 声明列表组件
function FruitList({ fruits, onSetFruit }) {
  return (
    <ul>
      {fruits.map((f) => (
        <li key={f} onClick={() => onSetFruit(f)}>
          {f}
        </li>
      ))}
    </ul>
  )
}
export default function HooksTest() {
  // 声明数组状态
  const [fruits, setFruits] = useState(['香蕉', '西瓜'])
  return (
    <div>
      {/*添加列表组件*/}
      <FruitList fruits={fruits} onSetFruit={setFruit} />
    </div>
  )
}

 

用户输入处理

 

// 声明输入组件
function FruitAdd(props) {
  // 输入内容状态及设置内容状态的方法
  const [pname, setPname] = useState('') // 键盘事件处理
  const onAddFruit = (e) => {
    if (e.key === 'Enter') {
      props.onAddFruit(pname)
      setPname('')
    }
  }
  return (
    <div>
      {' '}
      <input
        type="text"
        value={pname}
        onChange={(e) => setPname(e.target.value)}
        onKeyDown={onAddFruit}
      />{' '}
    </div>
  )
}
export default function HooksTest() {
  // ...
  return (
    <div>
      {' '}
      {/*添加水果组件*/}
      <FruitAdd onAddFruit={(pname) => setFruits([...fruits, pname])} />{' '}
    </div>
  )
}
useEffect 给函数组件增加了执行副作用操作的能力。
副作用( Side Effffect )是指一个 function 做了和本身运算返回值无关的事,比如:修改了全局变量、修改了传入的
参数、甚至是 console.log() ,所以 ajax 操作,修改 dom 都是算作副作用。
异步数据获取,更新 HooksTest.js
import { useEffect } from 'react'
useEffect(() => {
  setTimeout(() => {
    setFruits(['香蕉', '西瓜'])
  }, 1000)
}, []) // 设置空数组意为没有依赖,则副作用操作仅执行一次
如果副作用操作对某状态有依赖,务必添加依赖选项
 
useEffect(() => {
  const timer = setInterval(() => {
    console.log('msg')
  }, 1000)
  return function () {
    clearInterval(timer)
  }
}, [])
清除工作:有一些副作用是需要清除的,清除工作非常重要的,可以防止引起内存泄露
 

useReducer

useReducer useState 的可选项,常用于组件有复杂状态逻辑时,类似于 redux reducer 概念。
商品列表状态维护
useEffect(() => {
const timer = setInterval(() => {
console.log('msg');
}, 1000);
return function(){
clearInterval(timer);
}
}, []);
import { useReducer } from "react";
// 添加fruit状态维护fruitReducer
function fruitReducer(state, action) {
switch (action.type) {
case "init":
return action.payload;
case "add":
return [...state, action.payload];
default:
return state;
}
}
export default function HooksTest() {
// 组件内的状态不需要了
// const [fruits, setFruits] = useState([]);
// useReducer(reducer,initState)
const [fruits, dispatch] = useReducer(fruitReducer, []);
useEffect(() => {
setTimeout(() => {
// setFruits(["香蕉", "西瓜"]);
// 变更状态,派发动作即可
dispatch({ type: "init", payload: ["香蕉", "西瓜"] });
}, 1000);
}, []);
return (
<div>
{/*此处修改为派发动作*/}
<FruitAdd onAddFruit={pname => dispatch({type: 'add', payload: pname})} />
</div>
);
}

 

useContext

useContext 用于在快速在函数组件中导入上下文。
Hooks 相关拓展
  • 1. Hooks规则
  • 2. 自定义Hooks
  • 3. 一堆nb实现

推荐练习

1. 尝试利用 hooks 编写一个完整的购物应用
2. 基于 useReducer 的方式能否处理异步 action
 
import React, { useContext } from "react";
// 创建上下文
const Context = React.createContext();
export default function HooksTest() {
// ...
return (
{/* 提供上下文的值 */}
<Context.Provider value={{fruits,dispatch}}>
<div>
{/* 这里不再需要给FruitAdd传递状态mutation函数,实现了解耦 */}
<FruitAdd />
</div>
</Context.Provider>
);
}
function FruitAdd(props) {
// 使用useContext获取上下文
const {dispatch} = useContext(Context)
const onAddFruit = e => {
if (e.key === "Enter") {
// 直接派发动作修改状态
dispatch({ type: "add", payload: pname })
setPname("");
}
};
// ...
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值