@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
指令。
在上面的示例中,我们需要删除 node
和 job
字段上的 @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 使不可为空的字段/根可为空?
使用 LOG
或 NONE
操作时,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 检查要容易写得多。一旦我们了解了人们如何使用它,我们将考虑哪个值(如果有)应该作为默认值。