React 之元素與組件的區別

從問題出發

我被問過這樣一個問題:

想要實現一個 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 元素的介紹 [1]:

Babel 會把 JSX 轉譯成一個名爲 React.createElement() 函數調用。以下兩種示例代碼完全等效:

const element = <h1 class>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  />;

對於組件,我們要使用類似於 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>

我們可以視情況選擇合適的傳入方法。

參考資料

[1]

官方文檔中對 React 元素的介紹: https://zh-hans.reactjs.org/docs/introducing-jsx.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Dq--n6ZzI30Fd6vO_4ZQJQ