以Vue2为主要参考对比,辅以部分Vue3概念,向React 18迁移的CookBook
React中的父子组件通信、自定义事件、事件处理
父子组件通信
父传子
1、使用props传递属性
2、使用ref
父组件通过React.createRef()
创建Ref
,保存在实例属性myRef
上。父组件中,渲染子组件时,定义一个Ref
属性,值为刚创建的myRef
。
父组件调用子组件的myFunc
函数,传递一个参数,子组件接收到参数,打印出参数。
参数从父组件传递给子组件,完成了父组件向子组件通信。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import React, { Component, Fragment } from 'react';
class Son extends Component { myFunc(name) { console.log(name); } render() { return <></>; } }
export default class Father extends Component { constructor(props) { super(props); this.myRef = React.createRef(); }
componentDidMount() { this.myRef.current.myFunc('Jack'); } render() { return ( <> <Son ref={this.myRef} /> </> ); } }
|
子传父
1、使用回调函数,参见下文自定义事件
2、事件冒泡
点击子组件的button
按钮,事件会冒泡到父组件身上,触发父组件的onClick
函数,打印出Jack
。点击的是子组件,父组件执行函数,完成了子组件向父组件通信。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const Son = () => { return <button>点击</button>; };
const Father = () => { const sayName = name => { console.log(name); }; return ( <div onClick={() => sayName('Jack')}> <Son /> </div> ); };
export default Father;
|
自定义事件
在Vue中,我们通常会声明一组v-on与emit来进行自定义事件的传递。而在React中则可以通过props将事件处理器本身传递进子组件中用以调用。
Vue示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <template> <Coat @rape="rape"></Coat> </template> <script> export default { methods: { rape(e){ console.log(e) } } } </script>
<template> <button @click="$emit('rape','哼哼哼啊啊啊啊啊')">子组件雷普父组件</button> </template>
|
React示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const Test: FC<{ rape: Function }> = (props) => { return ( <button onClick={() => { props.rape('哼哼哼啊啊啊啊啊') }}>子组件雷普父组件</button> ) }
const App: FC = () => { const onRape = (text: string) => { console.log(text) } return ( <div className='App'> <Test rape={onRape}></Test> </div> ) }
|
事件处理
详解参见React的事件处理
React中的祖孙组件通信:Provider-Consumer
当组件嵌套层级过多时,使用props一层层地传递事件不是一个好主意。除了使用第三方状态库以外,还可以使用Provider-Consumer设计模式来解决这个问题。在Vue中,类似的模式是Provide-Inject。
相关阅读:React组件设计模式-Provider-Consumer
类组件示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| import React, { Component, createContext } from 'react' import './index.css'
const COLOR = ['#B5E61D', '#ED1C24', '#00A2E8', '#A349A4', '#B97A57', '#A349A4'] const Theme = createContext(COLOR[1])
export default class grandfather extends Component { state = { theme: COLOR[0] } changeColor = () => { this.setState({ theme: COLOR[Math.ceil(Math.random() * (COLOR.length - 1))] }) } render() { return ( <div className="grandfather"> <div>当前主题为:{this.state.theme}</div> <div style={{ color: this.state.theme }}>grandfather</div> <Theme.Provider value={this.state.theme}> <Father></Father> </Theme.Provider > <button className="fixed2" onClick={this.changeColor}>换肤</button> </div> ) } }
function Father(props) { return ( <div className="father"> <Theme.Consumer> { (theme) => { return <div style={{ color: theme }}>father</div> } } </Theme.Consumer> <Son></Son> </div> ) }
function Son(props) { return ( <div className="son"> <Theme.Consumer> { (theme) => { return <div style={{ color: theme }}>son</div> } } </Theme.Consumer> </div> ) }
|
函数式组件示例(使用hooks)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import React, { useState ,useContext, createContext } from 'react' import './index.css'
const COLOR = ['#B5E61D', '#ED1C24', '#00A2E8', '#A349A4', '#B97A57', '#A349A4'] const Theme = createContext('#B5E61D')
export default function Grandfather() { const [ theme, setTheme ] = useState(COLOR[0]) function changeColor() { setTheme(COLOR[Math.ceil(Math.random() * (COLOR.length - 1))]) } return ( <div className="grandfather"> <div>当前主题为:{theme}</div> <div style={{ color: theme }}>grandfather</div> <Theme.Provider value={theme}> <Father></Father> </Theme.Provider> <button className="fixed3" onClick={changeColor}>换肤</button> </div> ) } function Father(props) { return ( <div className="father"> // 在函数组件中一样可以使用Context.Consumer语法来拿数据 <Theme.Consumer> { (theme) => <div style={{ color: theme }}>father</div> } </Theme.Consumer> <Son></Son> </div> ) }
function Son(props) { const theme = useContext(Theme) return ( <div className="son"> <div style={{ color: theme }}>son</div> </div> ) }
|
实战:在文章分类页面中自定义筛选器
预期效果:Categories.tsx页面传入文章分类,点击筛选器的具体文章类别时可以动态修改Categories.tsx中的参数
文件结构:
1 2 3 4 5 6 7 8
| - src - components - CategoryList - CategoryList.tsx - CategoryListItem.tsx - Context.tsx - views - Categories.tsx
|
views/Categories.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import {FC, useState} from "react"; import FluidWrapper from "../Framework/FluidWrapper"; import CategoryList from "@/components/CategoryList/CategoryList"; import CategoryContext from "@/components/CategoryList/Context";
const listOpts = [{ id: 1, name: "iPhone", children: [{id: 10, name: "iPhone 13 mini"}, {id: 11, name: "iPhone 13"}, {id: 12, name: "iPhone 13 Pro"}, { id: 13, name: "iPhone 13 Pro Max" }] }, { id: 2, name: "MacBook Air", children: [{id: 20, name: "MacBook Air M1"}, {id: 21, name: "MacBook Air M2"},] }]
const Categories: FC = () => { const [choseId, setCategory] = useState(Number()); let setChoseId = (id: number) => { setCategory(id) } return ( <FluidWrapper> <CategoryContext.Provider value={{choseId, setChoseId}}> <CategoryList options={listOpts}/> </CategoryContext.Provider> <span>已选择的类别ID:{choseId}</span> </FluidWrapper> ) }
export default Categories;
|
components/CategoryList/Context.tsx
1 2 3 4 5 6 7
| import React from "react";
export default React.createContext({ choseId: 0, setChoseId: (id: number): void => { } })
|
components/CategoryList/CategoryList.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import {FC} from "react"; import CategoryListItem from "@/components/CategoryList/CategoryListItem";
interface CategoryListType { options: Array<CategoryListItemType> }
export interface CategoryListItemType { id: number, name: number | string, children?: Array<CategoryListItemType> }
const CategoryList: FC<CategoryListType> = ({options}) => { return ( <ul> {options.map(row => { return <li key={row.id}> <h3>{row.name}</h3> {row.children ? <CategoryListItem children={row.children}></CategoryListItem> : ""} </li> })} </ul> ) }
export default CategoryList;
|
components/CategoryList/CategoryListItem.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import {FC, useContext} from "react"; import CategoryContext from "@/components/CategoryList/Context"; import {CategoryListItemType} from "@/components/CategoryList/CategoryList"; import {animated} from "react-spring";
const CategoryListItem: FC<{ children: Array<CategoryListItemType> }> = ({children}) => { const context = useContext(CategoryContext) return ( <animated.ul> {children.map(item => ( <li key={item.id} onClick={() => { context.setChoseId(item.id) }}>{item.name}</li> ))} </animated.ul> ) } export default CategoryListItem;
|
在React组件中实现插槽
向组件的props里传一个DOM进去就可以了,这里是JSX!
在Vue中,我们可以像这样通过插槽来实现在自定义组件ComponentA中插入组件ComponentB:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| // ComponentA.vue <template> <div> <span>This is Component Yajuu</span> <slot></slot> </div> </template>
// Test.vue <template> <div> <ComponentA> <ComponentB></ComponentB> </ComponentA> </div> </template>
|
而在React中有一个props.children的概念,被父子件所包裹的子组件可以使用这个属性来获取。
扩展阅读:React组件设计模式-组合组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import React, { FC } from "react";
const ChildComponent: FC = () => ( <div> <span>This is ChildComponent</span> </div> )
const ParentComponent: FC<{ children?: React.ReactNode }> = (props) => ( <div> <span>This is ParentComponent</span> {props.children} </div> )
const Test: FC = () => { return ( <> <ParentComponent> <ChildComponent></ChildComponent> <ChildComponent></ChildComponent> </ParentComponent> </> ) } export default Test;
|