@required Directive
The @required
directive can be added to fields in your Relay queries to declare how null values should be handled at runtime. You can think of it as saying "if this field is ever null, its parent field is invalid and should be null".
When you have a GraphQL schema where many fields are nullable, a considerable amount of product code is needed to handle each field's potential "nullness" before the underlying data can be used. With @required
, Relay can handle some types of null checks before it returns data to your component, which means that any field you annotate with @required
will become non-nullable in the generated types for your response.
If a @required
field is null at runtime, Relay will "bubble" that nullness up to the field's parent. For example, given this query:
query MyQuery {
viewer {
name @required(action: LOG)
age
}
}
If name
is null, Relay would return { viewer: null }
. You can think of @required
in this instance as saying "viewer
is useless without a name
".
Action​
The @required
directive has a required action
argument which has three possible values:
NONE
(expected)​
This field is expected to be null sometimes.
LOG
(recoverable)​
This value is not expected to ever be null, but the component can still render if it is. If a field with action: LOG
is null, the Relay field logger will receive a missing_required_field.log
event.
THROW
(unrecoverable)​
This value should not be null, and the component cannot render without it. If a field with action: THROW
is null at runtime, the component which reads that field will throw during render. The error message includes both the owner and field path. Only use this option if your component is contained within an error boundary.
Locality​
A field's @required
status is local to the fragment where it is specified. This allows you to add/remove the directive without having to think about anything outside the scope of your component.
This choice reflects the fact that some components may be able to recover better from missing data than others. For example, a <RestaurantInfo />
component could probably render something sensible even if the restaurant's address is missing, but a <RestaurantLocationMap />
component might not.
However, all usages of the @required
directive on the same field in a single fragment must be consistent with their usage. This situation mostly occurs when selecting fields in inline fragments. For example, the following fragment would fail to compile:
fragment UserInfo on User {
job {
... on Actor {
certifications
}
... on Lawyer {
certifications @required(action: LOG)
}
}
}
The Relay compiler will give you an error like All references to a field must have matching @required declarations.
. To fix this, either set the @required
directive on each of the fields selected in the inline fragment or remove the directive entirely.
Chaining​
@required
directives can be chained to make a deeply nested field accessible after just one null check:
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} />
Note: If you use @required
on a top level field of a fragment, the object returned from useFragment
itself may become nullable. The generated types will reflect this.
When chaining @required
directives, the Relay compiler will help you from unintentionally creating a chain with a more severe action than intended. Consider the following fragment
fragment MyUser on User {
profile_picture @required(action: THROW) {
url @required(action: LOG)
}
}
In this example we want the component to THROW if the profile_picture
field is null but we only want to LOG an error if the url
field is null. But recall, Relay will "bubble" nullness up to the parent field, if the url
field is null it will then cause the profile_picture
field to become null as well. And once that happens, the component will THROW. If you implement a pattern like this, the Relay compiler will give you an error
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
To fix this, either change the profile_picture
to use action: LOG
or change the url
field to use action: THROW
.
Caveats with Connections​
There are currently some limitations in using the @required
and @connection
directives together. When you use the @connection
directive, Relay automatically inserts some additional fields into the connection, and those fields won't be generated with the @required
directive. This can result in inconsistencies if you use the @required
directive on fields in a Connection type. Consider the following example:
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)
}
}
}
}
}
Any usages of @required
on the node
field or any of its direct child fields will cause the Relay compiler to give you an error saying All references to a field must have matching @required declarations.
. In order to bypass this you'll need to remove the @required
directives on those fields.
In the above example, we'd need to remove the @required
directives on both the node
and job
fields, but the usage on the title
field would not create an error.
fragment FriendsList on User @refetchable(queryName: "FriendsListQuery") {
friends(after: $cursor, first: $count) @connection(key: "FriendsList_friends") {
edges {
node {
job {
title @required(action: LOG)
}
}
}
}
}
FAQ​
Why did @required make a non-nullable field/root nullable?​
When using the LOG
or NONE
actions, Relay will "bubble" a missing field up to its parent field or fragment root. This means that adding @required(action: LOG)
(for example) to a child of a non-nullable fragment root will cause the type of the fragment root to become nullable.
What happens if you use @required
in a plural field​
If a @required(action: LOG)
field is missing in a plural field, the item in the list will be returned as null. It will not cause the entire array to become null.. If you have any question about how it will behave, you can inspect the generated Flow types.
Why are @required fields in an inline fragment still nullable?​
Imagine a fragment like this:
fragment MyFrag on Actor {
... on User {
name @required(action: THROW)
}
}
It's possible that your Actor
will not be a User
and therefore not include a name
. To represent that in types, we generate a Flow type that looks like this: {name?: string}
.
If you encounter this issue, you can add a __typename
like this:
fragment MyFrag on Actor {
__typename
... on User {
name @required(action: THROW)
}
}
In this situation Relay will generate a union type like: {__typename: 'User', name: string} | {__typename: '%ignore this%}
. Now you can check the __typename
field to narrow your object's type down to one that has a non-nullable name
.
Why not implement this at the schema/server level?​
The "requiredness" of a field is actually a product decision and not a schema question. Therefore we need to implement the handling of it at the product level. Individual components need to be able to decide for themselves how to handle a missing value.
For example, if a notification is trying to show the price for a Marketplace listing, it could probably just omit the price and still render. If payment flow for that same listing is missing the price, it should probably blow up.
Another issue is that changes to the server schema are much more difficult to ship since they affect all existing clients across all platforms.
Basically every value returned by Relay is nullable. This is intentional since we want to be able to handle field-level errors whenever possible. If we lean into KillsParentOnException we would end up wanting to make basically every field use it and our apps would be becomes more brittle since errors which used to be small, become large.
Can (action: NONE)
be the default?​
On one hand action: NONE makes the most sense as a default (omitted action == no action). However, we are aware that whichever value we choose as the default will be considered the default action for engineers to choose since it's the path of least resistance.
We actually believe that in most cases LOG is the most ideal choice. It gives the component a chance to gracefully recover while also giving us signal that a part of our app is rendering in a sub-optimal way.
We debated making LOG the default action for that reason, but I think that's confusing as well.
So, for now we are planning to not offer a default argument. After all, it's still much less to write out than the equivalent manual null checks. Once we see how people use it we will consider what value (if any) should be the default.