React 之元素与组件的区别

简介: 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 元素的介绍


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>

我们可以视情况选择合适的传入方法。

目录
相关文章
|
5天前
|
前端开发 JavaScript 测试技术
React 分页组件 Pagination
本文介绍了如何在 React 中从零构建分页组件,涵盖基础概念、常见问题及解决方案。通过示例代码详细讲解了分页按钮的创建、分页按钮过多、初始加载慢、状态管理混乱等常见问题的解决方法,以及如何避免边界条件、性能优化和用户反馈等方面的易错点。旨在帮助开发者更好地理解和掌握 React 分页组件的开发技巧,提升应用的性能和用户体验。
24 0
|
9天前
|
移动开发 前端开发 API
React 拖拽组件 Drag & Drop
本文介绍了在 React 中实现拖拽功能的方法,包括使用原生 HTML5 Drag and Drop API 和第三方库 `react-dnd`。通过代码示例详细讲解了基本的拖拽实现、常见问题及易错点,帮助开发者更好地理解和应用拖拽功能。
31 9
|
4天前
|
前端开发 UED 开发者
React 分页组件 Pagination
本文介绍了如何在 React 中实现分页组件,从基础概念到常见问题及解决方案。分页组件用于将大量数据分成多个页面,提升用户体验。文章详细讲解了分页组件的基本结构、快速入门步骤、以及如何处理页面跳转不平滑、页码过多导致布局混乱、边界条件处理和数据加载延迟等问题。通过本文,读者可以全面了解并掌握 React 分页组件的开发技巧。
8 2
|
8天前
|
设计模式 前端开发 编译器
与普通组件相比,React 泛型组件有哪些优势?
与普通组件相比,React 泛型组件有哪些优势?
22 6
|
4天前
|
JavaScript 前端开发 算法
React 框架和 Vue 框架的区别是什么?
React框架和Vue框架都是目前非常流行的前端JavaScript框架,它们在很多方面存在区别
|
16天前
|
前端开发 JavaScript 安全
学习如何为 React 组件编写测试:
学习如何为 React 组件编写测试:
33 2
|
17天前
|
JavaScript 前端开发 算法
在性能上,React和Vue有什么区别
【10月更文挑战第23天】在性能上,React和Vue有什么区别
14 1
|
23天前
|
前端开发 JavaScript 测试技术
React 高阶组件 (HOC) 应用
【10月更文挑战第16天】高阶组件(HOC)是 React 中一种复用组件逻辑的方式,通过接受一个组件并返回新组件来实现。本文介绍了 HOC 的基础概念、核心功能和常见问题,包括静态方法丢失、ref 丢失、多个 HOC 组合和 props 冲突的解决方案,并提供了具体的 React 代码示例。通过本文,读者可以更好地理解和应用 HOC,提高代码的复用性和可维护性。
52 8
|
23天前
|
前端开发 JavaScript UED
react-router 里的 Link 标签和 a 标签有什么区别
`react-router` 中的 `Link` 标签与 HTML 中的 `a` 标签的主要区别在于:`Link` 是专门为 React 应用设计的,用于实现客户端路由导航,不会触发页面的重新加载,而 `a` 标签则会刷新整个页面。使用 `Link` 可以提升应用的性能和用户体验。
|
22天前
|
缓存 前端开发 JavaScript
前端serverless探索之组件单独部署时,利用rxjs实现业务状态与vue-react-angular等框架的响应式状态映射
本文深入探讨了如何将RxJS与Vue、React、Angular三大前端框架进行集成,通过抽象出辅助方法`useRx`和`pushPipe`,实现跨框架的状态管理。具体介绍了各框架的响应式机制,展示了如何将RxJS的Observable对象转化为框架的响应式数据,并通过示例代码演示了使用方法。此外,还讨论了全局状态源与WebComponent的部署优化,以及一些实践中的改进点。这些方法不仅简化了异步编程,还提升了代码的可读性和可维护性。