目录
前言1.1Props详解1.2 父子组件通信1.2.1 构建一个父子组件1.2.2 父组件给子组件传值1.2.3 父组件给子组件传值设置默认值1.2.4 使用prop-types属性验证1.3 props.children1.4 render props特性总结前言
在 React 开发中,组件复用是一个常见的需求。有时候,我们需要将某个组件的状态或行为共享给其他组件。传统的做法是通过高阶组件(Higher-Order Components, HOC)来实现,但这种方式有时会带来一些复杂性和可读性问题。Render Props 提供了一种更简洁、更灵活的方式来共享组件的状态和行为。本文将详细介绍 Render Props 的概念、实现方法及其应用场景。
1.1Props详解
props
是正常是外部传入的,组件内部也可以通过一些方式来初始化的设置,属性不能被组件自己更改,但是你可以通过父组件主动重新渲染的方式来传入新的 props
React 非常灵活,但它也有一个严格的规则:
所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
纯函数:输入一定,输出一定确定
总的来说,在使用一个组件的时候,可以把参数放在标签的属性当中,所有的属性都会作为组件 props
对象的键值。
通过箭头函数创建的组件,需要通过函数的参数来接收props
通过类创建的组件,需要通过 this.props
来接收
组件可以在其输出中引用其他组件。
这就可以让我们用同一组件来抽象出任意层次的细节。
按钮,表单,对话框,甚至整个屏幕的内容:在 React 应用程序中,这些通常都会以组件的形式表示。
1.2 父子组件通信
1.2.1 构建一个父子组件
src/index.js
import React from 'react'import ReactDOM from 'react-dom/client'// 引入时,后缀名可以省略,可以在webpack中配置import App from './01-App-parent-child'const root = ReactDOM.createRoot(document.getElementById('root'))root.render(<App />)
src/01-App-parent-child.jsx
import React from 'react'// ? 为什么react中组件的首字母必须大写?// 如果小写,被视为 html 固有的标签,而html固有标签如果没有,则不显示const Header = () => { return ( <header>react 核心库只关注于视图层</header> )}class Content extends React.Component { render () { return ( <div>react 16.8 推出了 react hooks</div> ) }}const Footer = () => { return ( <footer>react真的很简单</footer> )}class App extends React.Component { render () { return ( <div> <Header></Header> <Content></Content> <Footer></Footer> </div> ) }}export default App
1.2.2 父组件给子组件传值
src/index.js
import React from 'react'import ReactDOM from 'react-dom/client'// 引入时,后缀名可以省略,可以在webpack中配置// import App from './01-App-parent-child'import App from './02-App-parent-child-value'const root = ReactDOM.createRoot(document.getElementById('root'))root.render(<App />)
src/02-App-parent-child-value.jsx
import React from 'react'// 父组件在调用子组件的地方,添加自定义的属性,如果属性的值是变量,boolean类型,// number类型,对象,数组,null,undefined, 函数,需要使用 {} 包裹// 如果子组件是类组件,在子组件的内部,可以通过 this.props 访问到父组件传递的数据// 如果子组件是函数式组件,函数拥有默认参数为props,可以通过 props 访问到父组件传递的数据const Header = (props) => { console.log(props) // { name: 'React.js' } return ( <header>{ props.name } 核心库只关注于视图层</header> )}class Content extends React.Component { render () { console.log(this.props) // {version: 16.8} return ( <div>react { this.props.version } 推出了 react hooks</div> ) }}const Footer = ({ msg }) => { // 从props对象解构了msg return ( <footer>react真的很{ msg }</footer> )}class App extends React.Component { render () { return ( <div> <Header name="React.js"></Header> <Content version={ 16.8 }></Content> <Footer msg="简单"></Footer> </div> ) }}export default App
1.2.3 父组件给子组件传值设置默认值
src/index.js
import React from 'react'import ReactDOM from 'react-dom/client'// 引入时,后缀名可以省略,可以在webpack中配置// import App from './01-App-parent-child'// import App from './02-App-parent-child-value'import App from './03-App-parent-child-value-default'const root = ReactDOM.createRoot(document.getElementById('root'))root.render(<App />)
src/03-App-parent-child-value-default.jsx
import React from 'react'// 父组件在调用子组件的地方,添加自定义的属性,如果属性的值是变量,boolean类型,// number类型,对象,数组,null,undefined, 函数,需要使用 {} 包裹// 如果子组件是类组件,在子组件的内部,可以通过 this.props 访问到父组件传递的数据// 如果子组件是函数式组件,函数拥有默认参数为props,可以通过 props 访问到父组件传递的数据// 如果需要给子组件设置默认值// 不管是类组件 还是 函数式组件,在定义组件之后,添加defaultProps属性即可// 如果是类组件,还可以通过 类的静态属性 设置默认值const Header = (props) => { console.log(props) // { name: 'React.js' } return ( <header>{ props.name } 核心库只关注于视图层</header> )}Header.defaultProps = { name: 'React.js'}class Content extends React.Component { static defaultProps = { // 类的静态属性 version: 16.8 } render () { console.log(this.props) // {version: 16.8} return ( <div>react { this.props.version } 推出了 react hooks!</div> ) }}// Content.defaultProps = {// version: 16.8// }const Footer = ({ msg }) => { // 从props对象解构了msg return ( <footer>react真的很{ msg }</footer> )}Footer.defaultProps = { msg: '简单'}class App extends React.Component { render () { return ( <div> {/* <Header name="React.js"></Header> <Content version={ 16.8 }></Content> <Footer msg="简单"></Footer> */} <Header></Header> <Content></Content> <Footer></Footer> </div> ) }}export default App
1.2.4 使用prop-types属性验证
自 React v15.5 起,
React.PropTypes
已移入另一个包中。请使用 prop-types 库 代替。
$ cnpm i prop-types -Dimport PropTypes from 'prop-types';MyComponent.propTypes = {// 你可以将属性声明为 JS 原生类型,默认情况下// 这些属性都是可选的。optionalArray: PropTypes.array,optionalBool: PropTypes.bool,optionalFunc: PropTypes.func,optionalNumber: PropTypes.number,optionalObject: PropTypes.object,optionalString: PropTypes.string,optionalSymbol: PropTypes.symbol,// 任何可被渲染的元素(包括数字、字符串、元素或数组)// (或 Fragment) 也包含这些类型。optionalNode: PropTypes.node,// 一个 React 元素。optionalElement: PropTypes.element,// 一个 React 元素类型(即,MyComponent)。optionalElementType: PropTypes.elementType,// 你也可以声明 prop 为类的实例,这里使用// JS 的 instanceof 操作符。optionalMessage: PropTypes.instanceOf(Message),// 你可以让你的 prop 只能是特定的值,指定它为// 枚举类型。optionalEnum: PropTypes.oneOf(['News', 'Photos']),// 一个对象可以是几种类型中的任意一个类型optionalUnion: PropTypes.oneOfType([PropTypes.string,PropTypes.number,PropTypes.instanceOf(Message)]),// 可以指定一个数组由某一类型的元素组成optionalArrayOf: PropTypes.arrayOf(PropTypes.number),// 可以指定一个对象由某一类型的值组成optionalObjectOf: PropTypes.objectOf(PropTypes.number),// 可以指定一个对象由特定的类型值组成optionalObjectWithShape: PropTypes.shape({color: PropTypes.string,fontSize: PropTypes.number}),// An object with warnings on extra propertiesoptionalObjectWithStrictShape: PropTypes.exact({name: PropTypes.string,quantity: PropTypes.number}),// 你可以在任何 PropTypes 属性后面加上 `isRequired` ,确保// 这个 prop 没有被提供时,会打印警告信息。requiredFunc: PropTypes.func.isRequired,// 任意类型的必需数据requiredAny: PropTypes.any.isRequired,// 你可以指定一个自定义验证器。它在验证失败时应返回一个 Error 对象。// 请不要使用 `console.warn` 或抛出异常,因为这在 `oneOfType` 中不会起作用。customProp: function(props, propName, componentName) {if (!/matchme/.test(props[propName])) {return new Error( 'Invalid prop `' + propName + '` supplied to' + ' `' + componentName + '`. Validation failed.');}},// 你也可以提供一个自定义的 `arrayOf` 或 `objectOf` 验证器。// 它应该在验证失败时返回一个 Error 对象。// 验证器将验证数组或对象中的每个值。验证器的前两个参数// 第一个是数组或对象本身// 第二个是他们当前的键。customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {if (!/matchme/.test(propValue[key])) {return new Error( 'Invalid prop `' + propFullName + '` supplied to' + ' `' + componentName + '`. Validation failed.');}})};
src/index.js
import React from 'react'import ReactDOM from 'react-dom/client'// 引入时,后缀名可以省略,可以在webpack中配置// import App from './01-App-parent-child'// import App from './02-App-parent-child-value'// import App from './03-App-parent-child-value-default'import App from './04-App-parent-child-value-default-type'const root = ReactDOM.createRoot(document.getElementById('root'))root.render(<App />)
src/04-App-parent-child-value-default-type.jsx
import React from 'react'import PropTypes from 'prop-types'// 父组件在调用子组件的地方,添加自定义的属性,如果属性的值是变量,boolean类型,// number类型,对象,数组,null,undefined, 函数,需要使用 {} 包裹// 如果子组件是类组件,在子组件的内部,可以通过 this.props 访问到父组件传递的数据// 如果子组件是函数式组件,函数拥有默认参数为props,可以通过 props 访问到父组件传递的数据// 如果需要给子组件设置默认值// 不管是类组件 还是 函数式组件,在定义组件之后,添加defaultProps属性即可// 如果是类组件,还可以通过 类的静态属性 设置默认值// 如果需要验证父组件传递的数据的 数据类型// 需要通过第三方模块 prop-types 完成// 不管是类组件还是函数式组件,都是在定义组件之后,完成类型的校验// 通过 组件.propTypes 完成,写法为对象// key值为 父组件调用子组件的时候设置的 自定义的属性名// value 值为 PropTypes.数据类型// 如果自定义的属性值是必须得传递的,那么通过 PropTypes.数据类型.isRequired 完成// 如果自定义的属性值即可以是 number类型,也可以是stirng类型,// 通过 PropTypes.oneOfType([ PropTypes.number, PropTypes.string ]) 设置const Header = (props) => { console.log(props) // { name: 'React.js' } return ( <header>{ props.name } 核心库只关注于视图层</header> )}Header.defaultProps = { name: 'React.js'}Header.propTypes = { // 首字母不大写 // name: PropTypes.string name: PropTypes.string.isRequired }class Content extends React.Component { static defaultProps = { // 类的静态属性 version: 16.8 } render () { console.log(this.props) // {version: 16.8} return ( <div>react { this.props.version } 推出了 react hooks!</div> ) }}// Content.defaultProps = {// version: 16.8// }Content.propTypes = { // version: PropTypes.number version: PropTypes.oneOfType([ PropTypes.number, PropTypes.string ])}const Footer = ({ msg }) => { // 从props对象解构了msg return ( <footer>react真的很{ msg }</footer> )}Footer.defaultProps = { msg: '简单'}Footer.propTypes = { // msg: PropTypes.bool // Invalid prop `msg` of type `string` supplied to `Footer`, expected `boolean` msg: PropTypes.string}class App extends React.Component { render () { return ( <div> {/* <Header name="React.js"></Header> <Content version={ 16.8 }></Content> <Footer msg="简单"></Footer> */} <Header></Header> <Content></Content> <Footer></Footer> </div> ) }}export default App
1.3 props.children
我们知道使用组件的时候,可以嵌套。要在自定义组件中使用嵌套结构,就需要使用 props.children
。
等同于 vue中的 slot 插槽
src/index.js
import React from 'react'import ReactDOM from 'react-dom/client'// 引入时,后缀名可以省略,可以在webpack中配置// import App from './01-App-parent-child'// import App from './02-App-parent-child-value'// import App from './03-App-parent-child-value-default'// import App from './04-App-parent-child-value-default-type'import App from './05-App-props-children'const root = ReactDOM.createRoot(document.getElementById('root'))root.render(<App />)
src/05-App-props-children.jsx
import React from 'react'const Header = (props) => { return ( <header>1 { props.children }</header> )}class Content extends React.Component { render () { return ( <div>2 { this.props.children }</div> ) }}const Footer = ({ children }) => { return ( <footer>3 { children }</footer> )}class App extends React.Component { render () { return ( <div> <Header>react 核心库只关注于视图层</Header> <Content>react 16.8 推出了 react hooks</Content> <Footer>react真的很简单</Footer> </div> ) }}export default App
如果需要给组件添加多个元素,并且显示在多个位置,可以如下设置:
src/index.js
import React from 'react'import ReactDOM from 'react-dom/client'// 引入时,后缀名可以省略,可以在webpack中配置// import App from './01-App-parent-child'// import App from './02-App-parent-child-value'// import App from './03-App-parent-child-value-default'// import App from './04-App-parent-child-value-default-type'// import App from './05-App-props-children'import App from './06-App-mutiple-props-children'const root = ReactDOM.createRoot(document.getElementById('root'))root.render(<App />)
src/06-App-mutiple-props-children.jsx
import React from 'react'// vue中 使用 具名插槽(<slot name=""></slot>)// react 需要依靠 props.children 的下标const Header = (props) => { console.log(props) return ( <header> <div>这里输出1的值 { props.children[0] }</div> <div>这里输出2的值 { props.children[1] }</div> <div>这里输出3的值 { props.children[2] }</div> </header> )}class App extends React.Component { render () { return ( <div> <Header> <div>1111111</div> <div>2222222</div> <div>3333333</div> </Header> </div> ) }}export default App
实现类似vue的具名插槽,需要通过 props.children 的下标去访问
1.4 render props特性
使用 Render Props 来解决横切关注点(Cross-Cutting Concerns)
组件是 React 代码复用的主要单元,但如何将一个组件封装的状态或行为共享给其他需要相同状态的组件并不总是显而易见。
以下组件跟踪 Web 应用程序中的鼠标位置:
src/index.js
import React from 'react'import ReactDOM from 'react-dom/client'// 引入时,后缀名可以省略,可以在webpack中配置// import App from './01-App-parent-child'// import App from './02-App-parent-child-value'// import App from './03-App-parent-child-value-default'// import App from './04-App-parent-child-value-default-type'// import App from './05-App-props-children'// import App from './06-App-mutiple-props-children'import App from './07-App-mouse-tracker'const root = ReactDOM.createRoot(document.getElementById('root'))root.render(<App />)
src/07-App-mouse-tracker.jsx
还没有学习状态state以及事件处理,这里先用
import { Component } from 'react'// react的事件需要使用 小驼峰 onMouseMove// 原生js onmounsemove// react 事件满足两个条件: 第一必须是事件,第二this指向当前的组件class App extends Component { // react 类组件的初始化状态,类似于vue中的data state = { x: 0, y: 0 } render() { return ( <div style={ { width: '100vw', height: '100vh', backgroundColor: '#f66'} } onMouseMove = { (event) => { console.log(event) // 修改初始化值 this.setState({ x: event.clientX, y: event.clientY }) } }> <p> 当前鼠标的位置在,x:{ this.state.x },y: { this.state.y } </p> </div> ) }}export default App;
当光标在屏幕上移动时,组件在
<p>
中显示其坐标。现在的问题是:我们如何在另一个组件中复用这个行为?换个说法,若另一个组件需要知道鼠标位置,我们能否封装这一行为,以便轻松地与其他组件共享它?
render prop 是一个用于告知组件需要渲染什么内容的函数 prop。
src/index.js
import React from 'react'import ReactDOM from 'react-dom/client'// 引入时,后缀名可以省略,可以在webpack中配置// import App from './01-App-parent-child'// import App from './02-App-parent-child-value'// import App from './03-App-parent-child-value-default'// import App from './04-App-parent-child-value-default-type'// import App from './05-App-props-children'// import App from './06-App-mutiple-props-children'// import App from './07-App-mouse-tracker'import App from './08-App-render-props'const root = ReactDOM.createRoot(document.getElementById('root'))root.render(<App />)
src/08-App-render-props.jsx
import { Component } from 'react'// 渲染属性共享组件的状态// 在需要共享的组件(Mouse)上,添加一个render的自定义属性,该属性是一个自定义函数// 在自定义函数的内部返回需要共享给的那个组件(Cat)// 在需要共享的组件(Mouse)内部,通过 this.props.render() 或者 props.render() 即可调用,参数即为需要共享的状态// 那么在定义自定义render属性的函数内部,就会接收到 参数,通过返回的组件(Cat)传递该参数即可const Cat = ({ mounsePointer }) => { return ( <div style={ { position: 'fixed', left: mounsePointer.x, top: mounsePointer.y, backgroundColor: '#ccc', width: 100, height: 100 } }></div> )}class Mounse extends Component { state = { x: 0, y: 0 } render () { return ( <div style={ { width: '100vw', height: '100vh', backgroundColor: '#f66'} } onMouseMove = { (event) => { console.log(event) // 修改初始化值 this.setState({ x: event.clientX, y: event.clientY }) } }> <p> 当前鼠标的位置在,x:{ this.state.x },y: { this.state.y } </p> { this.props.render(this.state) } </div> ) }}class App extends Component { render () { return ( <div> {/* <Mounse></Mounse> <Cat ></Cat> */} <Mounse render = { (mounsePointer) => { return <Cat mounsePointer = { mounsePointer }/> } }></Mounse> </div> ) }}export default App
此案例实际上完成了react中子组件给父组件传值
总结
通过 Render Props,我们可以将组件的状态或行为轻松地共享给其他组件,从而提高代码的复用性和可维护性。本文通过一个具体的例子——鼠标位置跟踪组件,展示了如何使用 Render Props 实现状态共享。希望本文能够帮助读者更好地理解和应用 Render Props,提升 React 应用的开发效率。