@required 指令
@required
指令可以添加到你的 Relay 查詢中的欄位,以宣告在執行時應如何處理空值。你可以將其視為表示「如果此欄位為空值,則其父欄位無效且應為空值」。
當你有一個 GraphQL Schema,其中許多欄位都是可空的時,需要大量的產品程式碼來處理每個欄位潛在的「空值性」,然後才能使用底層資料。透過 @required
,Relay 可以在將資料返回到你的元件之前處理某些類型的空值檢查,這意味著**任何你用** @required
**註釋的欄位,在產生的回應型別中都將變成不可空的**。
如果 @required
欄位在執行時為空值,Relay 會將該空值「向上冒泡」到欄位的父欄位。例如,給定以下查詢
query MyQuery {
viewer {
name @required(action: LOG)
age
}
}
如果 name
為空值,Relay 將返回 { viewer: null }
。你可以將此例中的 @required
視為表示「沒有 name
,viewer
就沒有用」。
動作
@required
指令有一個必要的 action
參數,其有三個可能的值
NONE
(預期)
此欄位有時預期會是空值。
LOG
(可恢復)
此值預期永遠不會是空值,但如果它是空值,元件**仍然可以渲染**。如果 action: LOG
的欄位為空值,Relay 環境記錄器將收到一個類似這樣的事件
{
name: 'read.missing_required_field',
owner: string, // MyFragmentOrQueryName
fieldPath: string, // path.to.my.field
};
THROW
(不可恢復)
此值不應為空值,且元件**沒有它就無法渲染**。如果 action: THROW
的欄位在執行時為空值,則讀取該欄位的元件**將在渲染期間拋出錯誤**。錯誤訊息包括擁有者和欄位路徑。僅當你的元件包含在 錯誤邊界內時才使用此選項。
局部性
欄位的 @required
狀態**對於指定它的片段是局部的**。這允許你添加/移除該指令,而無需考慮元件範圍之外的任何事情。
此選擇反映了這樣一個事實,即某些元件可能比其他元件更能從遺失的資料中恢復。例如,即使餐廳的地址遺失,<RestaurantInfo />
元件也可能可以渲染一些合理的內容,但 <RestaurantLocationMap />
元件可能無法。
但是,單個片段中同一欄位上所有 @required
指令的使用方式必須與它們的使用方式一致。這種情況主要發生在選擇內聯片段中的欄位時。例如,以下片段將無法編譯
fragment UserInfo on User {
job {
... on Actor {
certifications
}
... on Lawyer {
certifications @required(action: LOG)
}
}
}
Relay 編譯器會給你一個類似這樣的錯誤:All references to a field must have matching @required declarations.
。要修正此問題,請在內聯片段中選取的每個欄位上設定 @required
指令,或完全移除該指令。
鏈接
@required
指令可以鏈接,以在僅進行一次空值檢查後即可存取深層巢狀的欄位
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
欄位為空值,元件會 THROW,但如果 url
欄位為空值,我們只希望記錄錯誤。但是請回想一下,如果 url
欄位為空值,Relay 會將空值「向上冒泡」到父欄位,這也會導致 profile_picture
欄位也變成空值。一旦發生這種情況,元件就會 THROW。如果你實作像這樣的模式,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 編譯器給你一個錯誤,指出 All references to a field must have matching @required declarations.
。為了繞過此問題,你需要移除這些欄位上的 @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)
欄位,則清單中的項目將會以空值返回。它不會導致整個陣列變成空值。如果你對其行為方式有任何疑問,可以檢查產生的 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
的型別。
為什麼不在 Schema/伺服器層級實作此功能?
欄位的「必要性」實際上是一個產品決策,而不是一個 Schema 問題。因此,我們需要在產品層級實作其處理。個別元件需要能夠自行決定如何處理遺失的值。
例如,如果通知嘗試顯示 Marketplace 清單的價格,它可能可以省略價格並仍然渲染。如果同一個清單的付款流程遺失價格,它可能會崩潰。
另一個問題是,對伺服器 Schema 的變更更難推送,因為它們會影響所有平台上的所有現有用戶端。
基本上,Relay 返回的每個值都是可空的。這是故意的,因為我們希望盡可能處理欄位層級的錯誤。如果我們傾向於使用 KillsParentOnException,我們最終會希望基本上每個欄位都使用它,並且我們的應用程式會變得更加脆弱,因為以前很小的錯誤會變得很大。
(action: NONE)
可以是預設值嗎?
一方面,action: NONE 最適合作為預設值(省略動作 == 沒有動作)。但是,我們知道無論我們選擇哪個值作為預設值,都會被認為是工程師選擇的預設動作,因為它是阻力最小的路徑。
我們實際上認為在大多數情況下,LOG 是最理想的選擇。它讓元件有機會優雅地恢復,同時也給我們一個訊號,表明我們的應用程式的某一部分正在以次優的方式渲染。
我們討論過讓 LOG 成為預設動作,但我覺得這樣也很容易混淆。
因此,目前我們計劃不提供預設參數。畢竟,它仍然比手動空值檢查要少寫得多。一旦我們看到人們如何使用它,我們就會考慮哪個值(如果有)應該是預設值。