React 组件:组件通信、受控组件&非受控组件、异步组件、HOC高阶组件

文章目录

✨参考资料

视频:BV1mcpPeMETt

文档:https://message163.github.io/react-docs/react/components/hoc.html

组件基本概念

组件 是 UI 的基本构建块。它们接受 props(输入数据)并返回 UI(通常是 JSX)。React 主要有两种类型的组件:

(1)函数组件(Function Component):一个接受 props 并返回 JSX 的普通 JavaScript 函数。

javascript 复制代码
// 一个简单的函数组件
const MyComponent = (props: { name: string }) => {
  return <div>Hello, {props.name}!</div>;
};

(2)类组件(Class Component):基于类的组件,继承自 React.Component,并实现一个 render() 方法来返回 JSX。

在类组件中,props 通过 this.props 访问,并且需要实现一个 render() 方法来返回 JSX。

javascript 复制代码
class MyComponent extends React.Component<{ name: string }> {
  render() {
    return <div>Hello, {this.props.name}!</div>;
  }
}

组件通信

props = properties 简称,属性

每一个父组件都可以提供一个属性给他的字组件

父子通信 props

1. props 基本用法

函数式组件第一个参数就是 props

  • 父 app.tsx
javascript 复制代码
import Card from './components/Card'
function App() {

  return (
    <>
      <Card title={'111'}></Card>
      <Card title={'222'}></Card>
    </>
  )
}

export default App
  • 子组件
javascript 复制代码
import './index.css'

export default function Card(props) {
    // 函数式组件第一个参数就是 props
    console.log(props);
    
    return <div className="card">
        <header>
            <div>{props.title}</div>
            <div>副标题</div>
        </header>
        <main>
            内容区域
        </main>
        <footer>
            <button>确认</button>
            <button>取消</button>
        </footer>
    </div>
}
泛型

props 默认是 any 类型,我们要指明

(1)写法 1:interface 安装给 props

javascript 复制代码
import './index.css'
interface Props {
    title: string
}
export default function Card(props: Props) { }

(2)写法 2:

React.FC<>

javascript 复制代码
import './index.css'

interface Props {
    title: string
}

const Card: React.FC<Props> = (props) => {
    return <div className="card">
        <header>
            <div>{props.title}</div>
            <div>副标题</div>
        </header>
        <main>
            内容区域
        </main>
        <footer>
            <button>确认</button>
            <button>取消</button>
        </footer>
    </div>
}

export default Card;
  • 两个写法的区别,之前 的版本中
    • 写法2,可以通过 Card.defaultProps 定义默认值,但现在废弃了
    • 写法2,props 自带一个 children

2. 默认值

两种方式 解构+默认值

(1)解构

javascript 复制代码
interface Props {
    title?: string
}

const Card: React.FC<Props> = ({title="默认标题"}) => {
    return <div className="card"><div>

(2)默认值,声明一个默认对象

核心代码 const {title} = {...defaultProps, ...props};

注意这里defaultProps一定要写在前面,因为后面的值会覆盖前面的值,意思是你传了值,我就用你的

javascript 复制代码
interface Props {
    title: string
}

const defaultProps: Partial<Props> = {
    title: '默认标题'
}

const Card: React.FC<Props> = (props) => {
    const {title} = {...defaultProps, ...props};
    // 注意这里defaultProps一定要写在前面
    // 因为后面的值会覆盖前面的值
    // 意思是你传了值,我就用你的
    return <div className="card">
        <header>
            <div>{title}</div>
            <div>副标题</div>
        </header>
    </div>
}

3. props.children

特殊的 props,类似于 vue 的插槽

  • 父元素 app.tsx
    直接在双标签中写

传一个就是一个对象

javascript 复制代码
<Card title={'111'}>
  <div>
    <section>
      <i>我是内容111</i>
    </section>
  </div>
</Card>

传很多个就是数组

javascript 复制代码
<Card title={'111'}>
 <div>
   <section>
     <i>我是内容111</i>
   </section>
 </div>
 <div>
   <section>
     <i>我是内容222</i>
   </section>
 </div>
</Card>
  • 子元素 Card/index.tsx
javascript 复制代码
interface Props {
    title?: string,
    children?: React.ReactNode
}

const defaultProps: Partial<Props> = {
    title: '默认标题'
}

const Card: React.FC<Props> = (props) => {
    const {title} = {...defaultProps, ...props};
    console.log(props.children);
    
    return <div className="card">
        <header>
            <div>{title}</div>
            <div>副标题</div>
        </header>
        <main>
            {props.children}
        </main>
        <footer>
            <button>确认</button>
            <button>取消</button>
        </footer>
    </div>
}

export default Card;

props 支持的类型

string title={'测试'}

number id={1}

boolean isGirl={false}

null empty={null}

undefined empty={undefined}

object obj={ { a: 1, b: 2 } }

array arr={[1, 2, 3]}

function cb={(a: number, b: number) => a + b}

JSX.Element element={<div>测试</div>}

子父通信 - 函数

  • 利用了函数的回调

    父组件传递一个函数给子组件,子组件在合适的时候调用

  • 父组件

javascript 复制代码
<Card callback={fn}></Card>
  • 子组件
javascript 复制代码
interface Props {
    title?: string
    children?: React.ReactNode
    callback?: (params: string) => void
}

const defaultProps: Partial<Props> = {
    title: '默认标题'
}

const Card: React.FC<Props> = (props) => {
    props.callback && props.callback('我是子组件')
    ...
}

兄弟组件通信

1. 原生浏览器

原理就是发布订阅设计模式

这里假设 在 Card 中发布,在 CardNew 中接收,当然反过来也是可以的

  • 父元素
javascript 复制代码
function App() {
  return (
    <>
      <Card title={'我是兄弟A'}></Card>
      <CardNew title={'我是兄弟B'}></CardNew>
    </>
  )
}
  • card
javascript 复制代码
interface Props {
    title?: string
    children?: React.ReactNode
    callback?: (params: string) => void
}

const defaultProps: Partial<Props> = {
    title: '默认标题'
}

const Card: React.FC<Props> = (props) => {
    //------------------------------------
    // 1. 创建自定义事件
    // 名字可以随便写,但是不要与原生事件重复
    const e = new Event('on-card')
    const clickTap = () => {
        e.params = {name: '我是参数'}
        window.dispatchEvent(e)
    }
    //------------------------------------
    props.callback && props.callback('我是子组件')

    const {title} = {...defaultProps, ...props};
    console.log(props.children);
    
    return <div className="card">
        <header>
            <div>{title}</div>
            <div>副标题</div>
        </header>
        <main>
            {props.children}
        </main>
        <footer>
            <button onClick={clickTap}>确认</button>
            <button>取消</button>
        </footer>
    </div>
}

export default Card;

// 全局声明
// 给所有 Event 类型都额外增加一个 params 属性
// 这样无论你在哪写 Event,TS 都会认为它一定带着 { name: string }。
declare global {
    interface Event {
        params: {name: string}
    }
}
  • cardnew
javascript 复制代码
interface Props {
    title?: string
    children?: React.ReactNode
    callback?: (params: string) => void
}

const defaultProps: Partial<Props> = {
    title: '默认标题'
}

const CardNew: React.FC<Props> = (props) => {
    //------------------------------------
    // -- 接收方
    window.addEventListener('on-card', e => {
        console.log('我是 cardnew 收到了', e)  
    })
    //------------------------------------
}

2. mitt

祖孙组件

useContext

见文章

https://blog.csdn.net/yavlgloss/article/details/155195979?spm=1001.2014.3001.5502

受控组件 & 非受控组件

受控组件 (Controlled Components) 和 非受控组件 (Uncontrolled Components) 是 React 中常见的两种表单组件的管理方式

  • 受控组件 更适合需要频繁交互、验证或者需要在应用中获取表单数据的场景,比如复杂的表单、动态表单等。

  • 非受控组件 更适合简单的场景,比如简单的表单、只有少量输入,且不需要实时监控表单值的场景。它通常更轻量,适合不需要太多状态管理的情况。

  • 表单元素

受控组件

受控组件一般是指表单元素

(1)表单的数据由React的 State 管理

(2)更新数据时,需要手动调用setState()方法,更新数据。

因为React没有类似于Vue的v-model,所以需要自己实现绑定事件。

javascript 复制代码
function App() {
  const [value, setValue] = useState('')
  const handleChange = (e) => {
    setValue(e.target.value)
  }
  return (
    <>
      <input type="text" value={value} onChange={handleChange}/>
      <h5>{value}</h5>
    </>
  )
}

非受控组件

非受控组件指的是该表单元素不受React的State管理,表单的数据由DOM管理。通过useRef()来获取表单元素的值。

javascript 复制代码
function App() {
  const value = 'haha'
  const inputRef = useRef(null);
  const handleChange = () => {
    console.log(inputRef.current?.value);
    
  }
  return (
    <>
      <input onChange={handleChange} type='text' defaultValue={value} ref={inputRef}/>
      <h5>{value}</h5>
    </>
  )
}

特殊的非受控组件

file

javascript 复制代码
function App() {
  const fileRef = useRef(null);
  const handleChange = () => {
    console.log(fileRef.current?.files);
    
  }
  return (
    <>
      <input type="file" ref={fileRef} onChange={handleChange}/>
    </>
  )
}

比如 可以等点击了按钮之后,才获取 input 的内容

javascript 复制代码
function UncontrolledComponent() {
  const inputRef = useRef(null);

  const handleSubmit = () => {
    alert(`输入的内容是:${inputRef.current.value}`);
  };

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={handleSubmit}>提交</button>
    </>
  );
}

异步组件

  • 用法
javascript 复制代码
<Suspense fallback={<div>Loading...</div>}>
  <这里写异步组件>
</Suspense>

异步组件没加载出来会是白屏,用户体验不佳,所以使用suspense,能为用户显示fallback的内容

异步组件加载

一般情况下写了一个组件,在app.tsx 中引入,然后 npm run build 打包

javascript 复制代码
import React from 'react';
import AsyncComponent from './components/Async';

function App() {
  return (
    <>
      <h1>app</h1>
      <AsyncComponent></AsyncComponent>
    </>
  )
}

打包会发现,这个组件被打包进了主包中,这样子就增加了主包的大小,因此需要拆分

使用异步组件的方式,这样就可以减小主包的一个大小

javascript 复制代码
import React, {lazy, Suspense} from 'react';
const AsyncComponent = lazy(() => import('./components/Async'))

function App() {
  return (
    <>
      <h1>app</h1>
      <Suspense fallback={<div>loading...</div>}>
        <AsyncComponent></AsyncComponent>
      </Suspense>
    </>
  )
}

这样 拆分出来之后,npm run build 打包就很小了

异步数据加载

写在public 目录下的数据,可以直接被访问到

  • 当加载的时候,展示骨架屏,当加载完成,展示 card
  • App.tsx
javascript 复制代码
import { Skeleton } from './components/Skeleton';
import Card from './components/Card';

function App() {
  return (
    <>
      <Suspense fallback={<Skeleton></Skeleton>}>
        <Card></Card>
      </Suspense>
    </>
  )
}
  • Card
    Suspense 的工作机制是基于use() 的异步操作。它会等待 use() 里的异步数据(dataPromise)加载完成,然后自动渲染出最终的组件。
javascript 复制代码
import { use } from 'react'
import './index.css'
interface Data {
   name: string
   age: number
   address: string
   avatar: string
}


const sleep = (ms: number) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('')
        }, ms)
    })
}

const getData = async () => {
    await sleep(2000)
    return await fetch('/data.json').then((res) => res.json())
}

const dataPromise = getData();

const Card: React.FC = () => {
   const { data } = use(dataPromise);
   return <div className="card">
      <header className="card-header">
         <div className="card-name">{data.name}</div>
         <div className="card-age">{data.age}</div>
      </header>
      <section className="card-content">
         <div className="card-address">{data.address}</div>
         <div className="card-avatar">
            <img width={50} height={50} src={data.avatar} alt="" />
         </div>
      </section>
   </div>;
};

export default Card;

HOC

Higher Order Component

高阶组件本质上是一个 函数,而不是组件。它接受一个组件作为输入,返回一个新的组件。这个新的组件可以复用原始组件的逻辑,并且能够添加新的功能或行为。你可以理解为高阶组件在 增强 原组件的能力。

注意事项

  1. HOC不会修改传入的组件,而是使用组合的方式,通过将原组件包裹在一个容器组件中来实现功能扩展
  2. 注意避免多层嵌套,一般HOC的嵌套层级不要超过3层
  3. HOC的命名规范:with开头,如withLoading、withAuth等

简单例子

javascript 复制代码
enum Role {
  ADMIN = 'admin',
  USER = 'user',
}

const withAuthorization = (role: Role) => {
  return (Component: React.FC) => {
    // 判断是否具有权限的函数
    const isAuthorized = (role: Role) => {
      return role === Role.ADMIN;
    }
    return (props: any) => {
      // 判断是否具有权限
      if (isAuthorized(role)) {
        //把props透传给组件
        return <Component {...props} />
      } else {
        // 没有权限则返回一个提示
        return <div>抱歉,您没有权限访问该页面</div>
      }
    }
  }
}
  1. 接受一个 role 作为参数:role: Role:在这里传入角色类型(ADMIN 或 USER),它决定了权限判断的条件。
  2. withAuthorization 返回一个接受 Component 组件作为参数的函数。这个返回的函数接收 Component 作为参数,并返回一个新的组件。
javascript 复制代码
const AdminPage = withAuthorization(Role.ADMIN)(() => {
  return <div>管理员页面</div> //有权限输出
})

const UserPage = withAuthorization(Role.USER)(() => {
  return <div>用户页面</div> //没有权限不输出
})

复杂例子

相关推荐
ywf121526 分钟前
前端的dist包放到后端springboot项目下一起打包
前端·spring boot·后端
恋猫de小郭34 分钟前
2026,Android Compose 终于支持 Hot Reload 了,但是收费
android·前端·flutter
hpoenixf7 小时前
2026 年前端面试问什么
前端·面试
还是大剑师兰特7 小时前
Vue3 中的 defineExpose 完全指南
前端·javascript·vue.js
泯泷7 小时前
阶段一:从 0 看懂 JSVMP 架构,先在脑子里搭出一台最小 JSVM
前端·javascript·架构
mengchanmian8 小时前
前端node常用配置
前端
华洛8 小时前
利好打工人,openclaw不是企业提效工具,而是个人助理
前端·javascript·产品经理
xkxnq8 小时前
第六阶段:Vue生态高级整合与优化(第93天)Element Plus进阶:自定义主题(变量覆盖)+ 全局配置与组件按需加载优化
前端·javascript·vue.js
A黄俊辉A9 小时前
vue css中 :global的使用
前端·javascript·vue.js
小码哥_常9 小时前
被EdgeToEdge适配折磨疯了,谁懂!
前端