从问题出发
我被问过这样一个问题:
想要实现一个 useTitle 方法,具体使用示例如下:
1function Header() { 2 const [Title, changeTitle] = useTitle(); 3 return ( 4 <div onClick={() => changeTitle('new title')}> 5 <Title /> 6 </div> 7 ) 8}
但在编写 useTitle 代码的时候却出了问题:
1function TitleComponent({title}) { 2 return <div>{title}</div> 3} 4 5function useTitle() { 6 const [title, changeTitle] = useState('default title'); 7 8 const Element = React.createElement(TitleComponent, {title}); 9 return [Element.type, changeTitle]; 10}
这段代码直接报错,连渲染都渲染不出来,如果是你,该如何修改这段代码呢?
元素与组件
其实这就是一个很典型的元素与组件如何区分和使用的问题。
元素
我们先看 React 官方文档中对 React 元素的介绍:
Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。以下两种示例代码完全等效:
1const element = <h1 className="greeting">Hello, world!</h1>; 2 3const element = React.createElement( 4 'h1', 5 {className: 'greeting'}, 6 'Hello, world!' 7);
React.createElement() 会预先执行一些检查,以帮助你编写无错代码,但实际上它创建了一个这样的对象:
1// 注意:这是简化过的结构 2const element = { 3 type: 'h1', 4 props: { 5 className: 'greeting', 6 children: 'Hello, world!' 7 } 8};
这些对象被称为 “React 元素”。它们描述了你希望在屏幕上看到的内容。
你看,React 元素其实就是指我们日常编写的 JSX 代码,它会被 Babel 转义为一个函数调用,最终得到的结果是一个描述 DOM 结构的对象,它的数据结构本质是一个 JS 对象。
在 JSX 中,我们是可以嵌入表达式的,就比如:
1const name = 'Josh Perez'; 2const element = <h1>Hello, {name}</h1>;
所以如果我们要使用一个 React 元素,那我们应该使用嵌入表达式这种方式:
1const name = <span>Josh Perez</span>; 2const element = <h1>Hello, {name}</h1>;
组件
那组件呢?组件有两种,函数组件和 class 组件:
1// 函数组件 2function Welcome(props) { 3 return <h1>Hello, {props.name}</h1>; 4} 5// class 组件 6class Welcome extends React.Component { 7 render() { 8 return <h1>Hello, {this.props.name}</h1>; 9 } 10}
那如何使用组件呢?
1const element = <Welcome name="Sara" />;
对于组件,我们要使用类似于 HTML 标签的方式进行调用,Babel 会将其转译为一个函数调用
1const element = React.createElement(Welcome, { 2 name: "Sara" 3});
所以你看,组件的数据结构本质是一个函数或者类,当你使用元素标签的方式进行调用时,函数或者类会被执行,最终返回一个 React 元素。
问题如何解决
尽管这些内容都来自于 React 官方文档,但如果你能清晰的了解到 React 元素和组件的差别,你已经可以解决开头的问题了。至少有两种方式可以解决,一种是返回 React 元素,一种是返回 React 组件
第一种我们返回 React 元素:
1const root = ReactDOM.createRoot(document.getElementById('root')); 2 3function Header() { 4 const [Title, changeTitle] = useTitle(); 5 // 这里因为返回的是 React 元素,所以我们使用 {} 的方式嵌入表达式 6 return ( 7 <div onClick={() => changeTitle('new title')}> 8 {Title} 9 </div> 10 ) 11} 12 13function TitleComponent({title}) { 14 return <div>{title}</div> 15} 16 17function useTitle() { 18 const [title, changeTitle] = useState('default title'); 19 20 // createElement 返回的是 React 元素 21 const Element = React.createElement(TitleComponent, {title}); 22 return [Element, changeTitle]; 23} 24 25root.render(<Header />);
第二种我们返回 React 组件:
1 2const root = ReactDOM.createRoot(document.getElementById('root')); 3 4function Header() { 5 const [Title, changeTitle] = useTitle(); 6 // 因为返回的是 React 组件,所以我们使用元素标签的方式调用 7 return ( 8 <div onClick={() => changeTitle('new title')}> 9 <Title /> 10 </div> 11 ) 12} 13 14function TitleComponent({title}) { 15 return <div>{title}</div> 16} 17 18function useTitle() { 19 const [title, changeTitle] = useState('default title'); 20 21 // 这里我们构建了一个函数组件 22 const returnComponent = () => { 23 return <TitleComponent title={title} /> 24 } 25 // 这里我们直接将组件返回出去 26 return [returnComponent, changeTitle]; 27} 28 29root.render(<Header />);
自定义内容
有的时候我们需要给组件传入一个自定义内容。
举个例子,我们实现了一个 Modal 组件,有确定按钮,有取消按钮,但 Modal 展示的内容为了更加灵活,我们提供了一个 props 属性,用户可以自定义一个组件传入其中,用户提供什么,Modal 就展示什么,Modal 相当于一个容器,那么,我们该怎么实现这个功能呢?
第一种实现方式
以下是第一种实现方式:
1function Modal({content}) { 2 return ( 3 <div> 4 {content} 5 <button>确定</button> 6 <button>取消</button> 7 </div> 8 ) 9} 10 11function CustomContent({text}) { 12 return <div>{text}</div> 13} 14 15<Modal content={<CustomContent text="content" />} />
根据前面的知识,我们可以知道,content 属性这里传入的其实是一个 React 元素,所以 Modal 组件的内部是用 {} 进行渲染。
第二种实现方式
但第一种方式,并不总能解决需求。有的时候,我们可能会用到组件内部的值。
就比如一个倒计时组件 Timer,依然提供了一个属性 content,用于自定义时间的展示样式,时间由 Timer 组件内部处理,展示样式则完全由用户自定义,在这种时候,我们就可以选择传入一个组件:
1function Timer({content: Content}) { 2 const [time, changeTime] = useState('0'); 3 4 useEffect(() => { 5 setTimeout(() => { 6 changeTime((new Date).toLocaleTimeString()) 7 }, 1000) 8 }, [time]) 9 10 return ( 11 <div> 12 <Content time={time} /> 13 </div> 14 ) 15} 16 17function CustomContent({time}) { 18 return <div style={{border: '1px solid #ccc'}}>{time}</div> 19} 20 21 22<Timer content={CustomContent} />
在这个示例中,我们可以看到 content 属性传入的是一个 React 组件 CustomContent,而 CustomContent 组件会被传入 time 属性,我们正是基于这个约定进行的 CustomContent 组件的开发。
而 Timer 组件内部,因为传入的是组件,所以使用的是 <Content time={time}/>进行的渲染。
第三种实现方式
在面对第二种实现方式的需求时,除了上面这种实现方式,还有一种称为 render props 的技巧,比第二种方式更常见一些,我们依然以 Timer 组件为例:
1function Timer({renderContent}) { 2 const [time, changeTime] = useState('0'); 3 4 useEffect(() => { 5 setTimeout(() => { 6 changeTime((new Date).toLocaleTimeString()) 7 }, 1000) 8 }, [time]) 9 10 // 这里直接调用传入的 renderContent 函数 11 return ( 12 <div> 13 {renderContent(time)} 14 </div> 15 ) 16} 17 18function CustomContent({time}) { 19 return <div style={{border: '1px solid #ccc'}}>{time}</div> 20} 21 22root.render(<Timer renderContent={(time) => { 23 return <CustomContent time={time} /> 24}} />);
鉴于我们传入的是一个函数,我们把 content 属性名改为了 renderContent,其实叫什么都可以。
renderContent 传入了一个函数,该函数接收 time 作为参数,返回一个 React 元素,而在 Timer 内部,我们直接执行了 renderContent 函数,并传入内部处理好的 time 参数,由此实现了用户使用组件内部值自定义渲染内容。
多说一句,除了放到属性里,我们也可以放到 children 里,是一样的:
1function Timer({children}) { 2 // ... 3 return ( 4 <div> 5 {children(time)} 6 </div> 7 ) 8} 9 10 11<Timer> 12 {(time) => { 13 return <CustomContent time={time} /> 14 }} 15</Timer>
我们可以视情况选择合适的传入方法。
React 系列
讲解 React 源码、React API 背后的实现机制,React 最佳实践、React 的发展与历史等,预计 50 篇左右,欢迎关注
如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
