本文为稀土技能社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

从问题出发

我被问过这样一个问题:

想要完成一个 useTitle 办法,具体运用示例如下:

function Header() {
    const [Title, changeTitle] = useTitle();
    return (
        <div onClick={() => changeTitle('new title')}>
          <Title />
        </div>
    )
}

但在编写 useTitle 代码的时分却出了问题:

function TitleComponent({title}) {
    return <div>{title}</div>
}
function useTitle() {
    const [title, changeTitle] = useState('default title');
    const Element = React.createElement(TitleComponent, {title});
    return [Element.type, changeTitle];
}

这段代码直接报错,连烘托都烘托不出来,如果是你,该怎样修改这段代码呢?

元素与组件

其实这就是一个很典型的元素与组件怎样区分和运用的问题。

元素

咱们先看 React 官方文档中对 React 元素的介绍:

Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。以下两种示例代码彻底等效:

const element = <h1 className="greeting">Hello, world!</h1>;
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

React.createElement() 会预先履行一些查看,以帮助你编写无错代码,但实际上它创建了一个这样的目标:

// 注意:这是简化过的结构
const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world!'
  }
};

这些目标被称为 “React 元素”。它们描绘了你希望在屏幕上看到的内容。

你看,React 元素其实就是指咱们日常编写的 JSX 代码,它会被 Babel 转义为一个函数调用,终究得到的结果是一个描绘 DOM 结构的目标,它的数据结构本质是一个 JS 目标。

在 JSX 中,咱们是能够嵌入表达式的,就比如:

const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;

所以如果咱们要运用一个 React 元素,那咱们应该运用嵌入表达式这种办法:

const name = <span>Josh Perez</span>;
const element = <h1>Hello, {name}</h1>;

组件

那组件呢?组件有两种,函数组件和 class 组件:

// 函数组件
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
// class 组件
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

那怎样运用组件呢?

const element = <Welcome name="Sara" />;

对于组件,咱们要运用类似于 HTML 标签的办法进行调用,Babel 会将其转译为一个函数调用

const element = React.createElement(Welcome, {
  name: "Sara"
});

所以你看,组件的数据结构本质是一个函数或者类,当你运用元素标签的办法进行调用时,函数或者类会被履行,终究回来一个 React 元素。

问题怎样处理

虽然这些内容都来自于 React 官方文档,但如果你能明晰的了解到 React 元素和组件的差别,你现已能够处理最初的问题了。至少有两种办法能够处理,一种是回来 React 元素,一种是回来 React 组件

第一种咱们回来 React 元素:

const root = ReactDOM.createRoot(document.getElementById('root'));
function Header() {
    const [Title, changeTitle] = useTitle();
    // 这儿由于回来的是 React 元素,所以咱们运用 {} 的办法嵌入表达式
    return (
        <div onClick={() => changeTitle('new title')}>
          {Title}
        </div>
    )
}
function TitleComponent({title}) {
    return <div>{title}</div>
}
function useTitle() {
    const [title, changeTitle] = useState('default title');
    // createElement 回来的是 React 元素
    const Element = React.createElement(TitleComponent, {title});
    return [Element, changeTitle];
}
root.render(<Header />);

第二种咱们回来 React 组件:


const root = ReactDOM.createRoot(document.getElementById('root'));
function Header() {
    const [Title, changeTitle] = useTitle();
    // 由于回来的是 React 组件,所以咱们运用元素标签的办法调用
    return (
        <div onClick={() => changeTitle('new title')}>
          <Title />
        </div>
    )
}
function TitleComponent({title}) {
    return <div>{title}</div>
}
function useTitle() {
    const [title, changeTitle] = useState('default title');
    // 这儿咱们构建了一个函数组件
    const returnComponent = () => {
    	return <TitleComponent title={title} />
    }
    // 这儿咱们直接将组件回来出去
    return [returnComponent, changeTitle];
}
root.render(<Header />);

自定义内容

有的时分咱们需要给组件传入一个自定义内容。

举个例子,咱们完成了一个 Modal 组件,有确定按钮,有取消按钮,但 Modal 展现的内容为了愈加灵活,咱们供给了一个 props 特点,用户能够自定义一个组件传入其间,用户供给什么,Modal 就展现什么,Modal 相当于一个容器,那么,咱们该怎样完成这个功用呢?

第一种完成办法

以下是第一种完成办法:

function Modal({content}) {
  return (
    <div>
      {content}
      <button>确定</button>
      <button>取消</button>
    </div>
  )
}
function CustomContent({text}) {
  return <div>{text}</div>
}
<Modal content={<CustomContent text="content" />} />

依据前面的常识,咱们能够知道,content 特点这儿传入的其实是一个 React 元素,所以 Modal 组件的内部是用 {} 进行烘托。

第二种完成办法

但第一种办法,并不总能处理需求。有的时分,咱们可能会用到组件内部的值。

就比如一个倒计时组件 Timer,仍然供给了一个特点 content,用于自定义时刻的展现款式,时刻由 Timer 组件内部处理,展现款式则彻底由用户自定义,在这种时分,咱们就能够选择传入一个组件:

function Timer({content: Content}) {
    const [time, changeTime] = useState('0');
    useEffect(() => {
        setTimeout(() => {
            changeTime((new Date).toLocaleTimeString())
	}, 1000)
    }, [time])
    return (
        <div>
          <Content time={time} />
        </div>
    )
}
function CustomContent({time}) {
    return <div style={{border: '1px solid #ccc'}}>{time}</div>
}
<Timer content={CustomContent} />

在这个示例中,咱们能够看到 content 特点传入的是一个 React 组件 CustomContent,而 CustomContent 组件会被传入 time 特点,咱们正是基于这个约定进行的 CustomContent 组件的开发。

而 Timer 组件内部,由于传入的是组件,所以运用的是 <Content time={time}/>进行的烘托。

第三种完成办法

在面对第二种完成办法的需求时,除了上面这种完成办法,还有一种称为 render props 的技巧,比第二种办法更常见一些,咱们仍然以 Timer 组件为例:

function Timer({renderContent}) {
    const [time, changeTime] = useState('0');
    useEffect(() => {
        setTimeout(() => {
            changeTime((new Date).toLocaleTimeString())
	}, 1000)
    }, [time])
  // 这儿直接调用传入的 renderContent 函数
    return (
        <div>
          {renderContent(time)}
        </div>
    )
}
function CustomContent({time}) {
    return <div style={{border: '1px solid #ccc'}}>{time}</div>
}
root.render(<Timer renderContent={(time) => {
    return <CustomContent time={time} />
}} />);

鉴于咱们传入的是一个函数,咱们把 content 特点名改为了 renderContent,其实叫什么都能够。

renderContent 传入了一个函数,该函数接收 time 作为参数,回来一个 React 元素,而在 Timer 内部,咱们直接履行了 renderContent 函数,并传入内部处理好的 time 参数,由此完成了用户运用组件内部值自定义烘托内容。

多说一句,除了放到特点里,咱们也能够放到 children 里,是相同的:

function Timer({children}) {
  	// ...
    return (
        <div>
          {children(time)}
        </div>
    )
}
<Timer>
  {(time) => {
    return <CustomContent time={time} />
  }}
</Timer>

咱们能够视情况选择合适的传入办法。

React 系列

  1. React 之 createElement 源码解读

React 系列的预热系列,带我们从源码的视点深入了解 React 的各个 API 和履行进程,全目录不知道多少篇,预计写个 50 篇吧。