文档 / rescript-react / 组件 & Props
Edit

组件 & Props

组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。本章节旨在介绍组件的相关理念。

组件是什么

React 组件是一个描述 UI 元素的函数,它接收一个 props 对象(属性对象,描述 UI 动态部分的数据)作为参数,并返回一个 React.element

这个概念的好处是你可以只关注输入与输出。组件函数接收一些数据并返回一些不透明的 React.element,这些 React.element 由 React 框架管理并用来渲染 UI。

如果你想要了解更多关于组件接口实现的底层细节,请参考 超越 JSX 页面。

组件示例

让我们从第一个示例开始,看看 ReScript React 组件是什么样子的:

ReScriptJS Output
// src/Greeting.res
@react.component
let make = () => {
  <div>
    {React.string("Hello ReScripters!")}
  </div>
}

重要: 始终确保将组件函数命名为 make,并且不要忘记添加 @react.component 装饰器。

我们创建了 Greeting.res 文件,它包含一个不接收任何 props 的 make 函数(这个函数不接收任何参数),该函数返回一个在渲染的 DOM 中代表 <div> Hello ReScripters! </div>React.element

你还可以在 JS 输出中看到我们创建的函数被直接转译成了纯 JS 版本的 ReactJS 组件。请注意 <div> 是如何被转换为 JavaScript 中的 React.createElement("div",...) 调用的。

定义 Props

在 ReactJS 中,props 通常被描述为单个 props 对象。而在 ReScript 中,我们使用标签参数来定义 props 参数。这是一个示例:

ReScriptJS Output
// src/Article.res
@react.component
let make = (~title: string, ~visitorCount: int, ~children: React.element) => {
  let visitorCountMsg = "You are visitor number: " ++ Belt.Int.toString(visitorCount);
  <div>
    <div> {React.string(title)} </div>
    <div> {React.string(visitorCountMsg)} </div>
    children
  </div>
}

可选参数

我们也可以充分利用标签参数的能力来定义可选参数:

ReScriptJS Output
// Greeting.res
@react.component
let make = (~name: option<string>=?) => {
  let greeting = switch name {
    | Some(name) => "Hello " ++ name ++ "!"
    | None => "Hello stranger!"
  }
  <div> {React.string(greeting)} </div>
}

注意@react.component 装饰器隐式地将最后一个参数 () 添加到了 make 函数中(因此我们无需自己添加)。

在 JSX 中,你可以使用一些特殊的语法来使用可选的 props:

ReScriptJS Output
let name = Some("Andrea")

<Greeting ?name />

特殊的 keyref 属性

你不能定义任何名为 keyref 的属性。React 以不同的方式处理这些属性,当你尝试在组件函数中定义 ~key~ref 参数时,编译器将会生成一个错误。

查看对应的 数组 & Keys转发 React Refs 章节来获取更多信息。

处理无效的属性名称(如关键字)

type 这样的属性名(如 <input type="text" />)在语法上是无效的,因为 type 是 ReScript 中的保留关键字。使用 <input type_="text" /> 代替。

对于 aria-*,使用驼峰式,例如 ariaLabel。对于 DOM 组件,我们会在底层将其翻译成 aria-label

对于 data-*,这有点棘手;带有 - 的单词在 ReScript 中是无效的。当你确实需要使用它们时,例如 <div data-name="click me" />,请参阅 React.cloneElementReact.createDOMElementVariadic 章节。

Children Props

在 React 中 props.children 是一个特殊属性,用于表示父元素中的嵌套元素:

RES
let element = <div> child1 child2 </div>

默认情况下,每当你像上面的表达式一样传递子元素时,children 都将被视为 React.element

ReScriptJS Output
module MyList = {
  @react.component
  let make = (~children: React.element) => {
    <ul>
      children
    </ul>
  }
}

<MyList>
  <li> {React.string("Item 1")} </li>
  <li> {React.string("Item 2")} </li>
</MyList>

有趣的是,无论你传递一个还是多个元素,React 总是会将其子元素折叠为单个 React.element

当然也可以重新定义 children 的类型。下面是一些示例:

组件的子元素必须是 string

RES
module StringChildren = { @react.component let make = (~children: string) => { <div> {React.string(children)} </div> } } <StringChildren> "My Child" </StringChildren> // This will cause a type check error // 这会引起类型检查错误 <StringChildren/>

组件的子元素是可选的 React.element

RES
module OptionalChildren = { @react.component let make = (~children: option<React.element>=?) => { <div> {switch children { | Some(element) => element | None => React.string("No children provided") }} </div> } } <div> <OptionalChildren /> <OptionalChildren> <div /> </OptionalChildren> </div>

不允许有子元素的组件:

RES
module NoChildren = { @react.component let make = () => { <div> {React.string("I don't accept any children params")} </div> } } // The compiler will raise a type error here // 这会引起类型检查错误 <NoChildren> <div/> </NoChildren>

children props 很容易被滥用为建模层次结构的方法,例如,<List> <ListHeader/> <Item/> </List>List 应该只允许 ItemListHeader 元素),但这种约束很难实行,因为所有组件最终都是 React.element,所以这需要在 List 中添加臭名昭著的运行时检查来验证所有的子元素实际上是 ItemListHeader 类型。

解决此类问题的最好方法是使用普通 props,而不是 children,例如 <List header="..." items=[{id: "...", text: "..."}]/>。这种方式很容易对约束进行类型检查,并且让组件的使用者脱离一遍遍记忆组件间约束的苦海。

children 的最佳用例是传递没有任何语义或实现细节的 React.element

Props & 类型推断

ReScript 的类型系统非常擅长于根据 prop 的使用情况来推断其类型。

对于简单的, 作用域恰当的使用,或为了实验目的,是可以省略类型注释的:

RES
// Button.res @react.component let make = (~onClick, ~msg, ~children) => { <div onClick> {React.string(msg)} children </div> }

在上面的示例中,onClick 被推断为 ReactEvent.Mouse.t => unitmsg 被推断为 stringchildren 被推断为 React.element。当你将值转发到一些较小的(私有作用域)函数时,类型推断非常有用。

尽管类型推断帮我们避免了大量的键盘输入,我们仍然推荐你明确地写出 props 的类型(就像任何公共 API 一样),这样可以获得更好的类型可见性来防止令人困惑的类型错误。

在 JSX 中使用组件

每一个 ReScript 组件都可以在 JSX 中使用。例如,如果我们想要在 App 组件中使用 Greeting 组件,我们可以这么做:

ReScriptJS Output
// src/App.res

@react.component
let make = () => {
  <div>
    <Greeting/>
  </div>
}

注意: React 组件首字母大写;原生 DOM 元素首字母小写,像是 divbutton。更多关于 JSX 细节和代码转换的信息可以在我们的 JSX 语言手册部分找到。

手写组件

你不需要使用 @react.component 装饰器来编写可以在 JSX 中使用的组件。相反,你可以编写一对 makePropsmake 函数,它们的类型分别是 makeProps: 'a => propsmake: props => React.element,它们将共同作为 React 组件工作。

这适用于你自己版本的 @obj,或其他接受命名参数并返回单个 props 结构的函数。例如:

ReScriptJS Output
module Link = {
  type props = {"href": string, "children": React.element};
  @obj external makeProps:(
    ~href: string,
    ~children: React.element,
    unit) => props = ""

  let make = (props: props) => { 
    <a href={props["href"]}>
     {props["children"]}
    </a>
  }
}

<Link href="/docs"> {React.string("Docs")} </Link>

关于 @react.component 装饰器的细节以及它生成接口的更多信息可以在我们的 超越 JSX 章节找到。

子模块组件

我们也可以使用子模块来表达 React 组件,这使我们可以方便地构建更加复杂的 UI,而无需为每个复合组件创建多个文件(组件可能只会被父组件使用):

RES
// src/Button.res module Label = { @react.component let make = (~title: string) => { <div className="myLabel"> {React.string(title)} </div> } } @react.component let make = (~children) => { <div> <Label title="Getting Started" /> children </div> }

上面定义的 Button.res 文件现在包含了一个 Label 组件,它也可以被其他组件使用,或通过编写完整限定的模块名(<Button.Label title="My Button"/>),或通过模块别名来简写完整的限定符:

RES
module Label = Button.Label let content = <Label title="Test"/>

为组件添加名称

因为组件实际上是一对函数,为了在 JSX 中使用,它们必须属于一个模块。用模块来识别用途是很有意义的。@react.component 会根据你所在的模块自动为你添加名称。

RES
// File.res // will be named `File` in dev tools // 在开发工具中会被命名为 `File` @react.component let make = ... // will be named `File$component` in dev tools // 在开发工具中会被命名为 `File$component` @react.component let component = ... module Nested = { // will be named `File$Nested` in dev tools // 在开发工具中会被命名为 `File$Nested` @react.component let make = ... };

如果你需要为高阶组件设置动态名称,或者为组件设置你自己的名称,你可以使用 React.setDisplayName(make, "NameThatShouldBeInDevTools")

技巧和诀窍

  • 从一个组件文件开始。随着组件的增长,运用子模块组件的能力。在真正需要时才考虑将组件拆分为多个文件。

  • 保持你的目录结构扁平化。例如,使用 ArticleHeader.res 而不是 article/Header.res。文件名在代码库中是唯一的,所以文件名往往像是非常具体的 ArticleUserHeaderCard.res,这未必是件坏事,因为文件名清楚地表达了组件的意图,并使其可以轻松地在整个代码库中查找、匹配、重构。