组件 & Props
组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。本章节旨在介绍组件的相关理念。
组件是什么
React 组件是一个描述 UI 元素的函数,它接收一个 props
对象(属性对象,描述 UI 动态部分的数据)作为参数,并返回一个 React.element
。
这个概念的好处是你可以只关注输入与输出。组件函数接收一些数据并返回一些不透明的 React.element
,这些 React.element
由 React 框架管理并用来渲染 UI。
如果你想要了解更多关于组件接口实现的底层细节,请参考 超越 JSX 页面。
组件示例
让我们从第一个示例开始,看看 ReScript React 组件是什么样子的:
重要: 始终确保将组件函数命名为 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 参数。这是一个示例:
// 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>
}
可选参数
我们也可以充分利用标签参数的能力来定义可选参数:
注意: @react.component
装饰器隐式地将最后一个参数 ()
添加到了 make
函数中(因此我们无需自己添加)。
在 JSX 中,你可以使用一些特殊的语法来使用可选的 props:
特殊的 key
与 ref
属性
你不能定义任何名为 key
或 ref
的属性。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.cloneElement 或 React.createDOMElementVariadic 章节。
Children Props
在 React 中 props.children
是一个特殊属性,用于表示父元素中的嵌套元素:
RESlet element = <div> child1 child2 </div>
默认情况下,每当你像上面的表达式一样传递子元素时,children
都将被视为 React.element
:
有趣的是,无论你传递一个还是多个元素,React 总是会将其子元素折叠为单个 React.element
。
当然也可以重新定义 children
的类型。下面是一些示例:
组件的子元素必须是 string
:
RESmodule 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
:
RESmodule 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>
不允许有子元素的组件:
RESmodule 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
应该只允许 Item
或 ListHeader
元素),但这种约束很难实行,因为所有组件最终都是 React.element
,所以这需要在 List
中添加臭名昭著的运行时检查来验证所有的子元素实际上是 Item
或 ListHeader
类型。
解决此类问题的最好方法是使用普通 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 => unit
,msg
被推断为 string
,children
被推断为 React.element
。当你将值转发到一些较小的(私有作用域)函数时,类型推断非常有用。
尽管类型推断帮我们避免了大量的键盘输入,我们仍然推荐你明确地写出 props 的类型(就像任何公共 API 一样),这样可以获得更好的类型可见性来防止令人困惑的类型错误。
在 JSX 中使用组件
每一个 ReScript 组件都可以在 JSX 中使用。例如,如果我们想要在 App
组件中使用 Greeting
组件,我们可以这么做:
注意: React 组件首字母大写;原生 DOM 元素首字母小写,像是 div
或 button
。更多关于 JSX 细节和代码转换的信息可以在我们的 JSX 语言手册部分找到。
手写组件
你不需要使用 @react.component
装饰器来编写可以在 JSX 中使用的组件。相反,你可以编写一对 makeProps
与 make
函数,它们的类型分别是 makeProps: 'a => props
、make: props => React.element
,它们将共同作为 React 组件工作。
这适用于你自己版本的 @obj
,或其他接受命名参数并返回单个 props 结构的函数。例如:
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"/>
),或通过模块别名来简写完整的限定符:
RESmodule 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
,这未必是件坏事,因为文件名清楚地表达了组件的意图,并使其可以轻松地在整个代码库中查找、匹配、重构。