跳至主要内容
版本: v18.0.0

@required 指令

@required 指令可以添加到 Relay 查询中的字段,以声明在运行时如何处理 null 值。您可以将其理解为“如果此字段在任何时候为 null,则其父字段无效,应为 null”。

当您的 GraphQL 模式中存在许多可为空的字段时,需要大量的产品代码来处理每个字段的潜在“空值”,然后才能使用底层数据。使用 @required,Relay 可以在将数据返回给您的组件之前处理某些类型的 null 检查,这意味着任何您使用 @required 注释的字段将在您的响应的生成类型中变为不可为空

如果 @required 字段在运行时为 null,Relay 将将该 null 值“冒泡”到该字段的父级。例如,给定此查询

query MyQuery {
viewer {
name @required(action: LOG)
age
}
}

如果 name 为 null,Relay 将返回 { viewer: null }。您可以将此情况下的 @required 理解为“viewer 在没有 name 的情况下是无用的”。

动作

@required 指令有一个必需的 action 参数,它有三个可能的值

NONE(预期)

预计此字段有时为 null。

LOG(可恢复)

不应期望此值为 null,但如果为 null,组件仍然可以渲染。如果使用 action: LOG 的字段为 null,Relay 环境日志记录器将收到一个类似于以下内容的事件

{
name: 'read.missing_required_field',
owner: string, // MyFragmentOrQueryName
fieldPath: string, // path.to.my.field
};

THROW(不可恢复)

此值不应为 null,并且组件在没有它的情况下无法渲染。如果使用 action: THROW 的字段在运行时为 null,读取该字段的组件将在渲染期间抛出异常。错误消息包含所有者和字段路径。仅当您的组件包含在错误边界中时使用此选项。

局部性

字段的 @required 状态仅限于指定它的片段。这样您就可以添加/删除该指令,而无需考虑组件范围之外的任何事项。

此选择反映了某些组件可能比其他组件更好地从丢失的数据中恢复。例如,<RestaurantInfo /> 组件可能可以正常渲染,即使餐厅的地址丢失,但 <RestaurantLocationMap /> 组件可能不行。

但是,在单个片段中对同一字段使用 @required 指令的所有用法都必须与其用法一致。这种情况主要发生在内联片段中选择字段时。例如,以下片段将无法编译

fragment UserInfo on User {
job {
... on Actor {
certifications
}
... on Lawyer {
certifications @required(action: LOG)
}
}
}

Relay 编译器将给出类似于 所有对字段的引用必须具有匹配的 @required 声明。的错误。要解决此问题,请在内联片段中选择的每个字段上设置 @required 指令,或完全删除该指令。

链接

@required 指令可以链接起来,只需一次 null 检查即可访问深度嵌套的字段

const user = useFragment(graphql`
fragment MyUser on User {
name @required(action: LOG)
profile_picture @required(action: LOG) {
url @required(action: LOG)
}
}`, key);
if(user == null) {
return null;
}
return <img src={user.profile_picture.url} alt={user.name} />

注意:如果您在片段的顶层字段上使用 @required,则从 useFragment 返回的对象本身可能会变为可为空。生成的类型将反映这一点。

在链接 @required 指令时,Relay 编译器将帮助您避免无意中创建一个比预期更严重的动作链。请考虑以下片段

fragment MyUser on User {
profile_picture @required(action: THROW) {
url @required(action: LOG)
}
}

在此示例中,我们希望如果 profile_picture 字段为 null,则组件将抛出异常,但如果 url 字段为 null,我们只希望记录错误。但是请记住,Relay 将将 null 值“冒泡”到父字段,如果 url 字段为 null,则会使 profile_picture 字段也变为 null。一旦发生这种情况,组件将抛出异常。如果您实现了这种模式,Relay 编译器将给您一个错误

A @required field may not have an `action` less severe than that of its @required parent. This @required directive should probably have `action: LOG` so that it can match its parent

要解决此问题,请将 profile_picture 更改为使用 action: LOG,或将 url 字段更改为使用 action: THROW

连接的注意事项

目前在将 @required@connection 指令一起使用时存在一些限制。当您使用 @connection 指令时,Relay 会自动将一些附加字段插入到连接中,这些字段不会使用 @required 指令生成。如果您在 Connection 类型中的字段上使用 @required 指令,这会导致不一致。请考虑以下示例

fragment FriendsList on User @refetchable(queryName: "FriendsListQuery") {
friends(after: $cursor, first: $count) @connection(key: "FriendsList_friends") {
edges {
node @required(action: LOG) {
job @required(action: LOG) {
title @required(action: LOG)
}
}
}
}
}

node 字段或其任何直接子字段上使用 @required 将导致 Relay 编译器给您一个错误,提示 所有对字段的引用必须具有匹配的 @required 声明。。为了绕过此问题,您需要删除这些字段上的 @required 指令。

在上面的示例中,我们需要删除 nodejob 字段上的 @required 指令,但对 title 字段的使用不会产生错误。

fragment FriendsList on User @refetchable(queryName: "FriendsListQuery") {
friends(after: $cursor, first: $count) @connection(key: "FriendsList_friends") {
edges {
node {
job {
title @required(action: LOG)
}
}
}
}
}

常见问题解答

为什么 @required 使不可为空的字段/根可为空?

使用 LOGNONE 操作时,Relay 将将丢失的字段“冒泡”到其父字段或片段根。这意味着将 @required(action: LOG)(例如)添加到不可为空的片段根的子字段中,将导致片段根的类型变为可为空。

如果您在复数字段中使用 @required 会发生什么?

如果复数字段中缺少 @required(action: LOG) 字段,则列表中的项目将返回为 null。它不会导致整个数组变为 null。如果您对它的行为有任何疑问,可以检查生成的 Flow 类型。

为什么内联片段中的 @required 字段仍然可为空?

想象一下这样的片段

fragment MyFrag on Actor {
... on User {
name @required(action: THROW)
}
}

您的 Actor 可能不是 User,因此不包含 name。为了在类型中表示这一点,我们生成一个类似于以下的 Flow 类型:{name?: string}

如果您遇到此问题,可以添加 __typename,如下所示

fragment MyFrag on Actor {
__typename
... on User {
name @required(action: THROW)
}
}

在这种情况下,Relay 将生成一个联合类型,例如:{__typename: 'User', name: string} | {__typename: '%ignore this%}。现在您可以检查 __typename 字段,将对象的类型缩小到具有不可为空 name 的类型。

为什么不在模式/服务器级别实现它?

字段的“必需性”实际上是一个产品决策,而不是模式问题。因此,我们需要在产品级别实现对它的处理。各个组件需要能够自行决定如何处理丢失的值。

例如,如果通知尝试显示市场列表的价格,它可能会忽略价格,但仍然可以渲染。如果对同一列表的付款流程缺少价格,它可能应该崩溃。

另一个问题是,服务器模式的更改更难发布,因为它们会影响所有平台上的所有现有客户端。

基本上,Relay 返回的每个值都是可为空的。这是故意的,因为我们希望能够尽可能地处理字段级错误。如果我们依赖于 KillsParentOnException,最终会希望让几乎每个字段都使用它,并且我们的应用程序会变得更加脆弱,因为以前很小的错误会变得很大。

(action: NONE) 可以作为默认值吗?

一方面,action: NONE 作为默认值最有意义(省略的动作 == 无动作)。但是,我们知道,无论我们选择哪个值作为默认值,工程师都会认为它是默认操作,因为这是阻力最小的路径。

我们实际上认为,在大多数情况下,LOG 是最理想的选择。它使组件有机会优雅地恢复,同时也能让我们了解应用程序的某个部分正在以次优方式渲染。

我们讨论过将 LOG 作为默认操作,但我们认为这也很令人困惑。

因此,目前我们计划不提供默认参数。毕竟,它仍然比等效的手动 null 检查要容易写得多。一旦我们了解了人们如何使用它,我们将考虑哪个值(如果有)应该作为默认值。