主要宣告資料需求的方式是藉由 Relay.Container
— 一個讓 React component 把它們的資料需求編碼的 higher-order React component。
跟 React component 的 render
方法不直接去改動原生的 view 類似,Relay container 也不直接抓取資料。作為替代,container 宣告一份 render 需要的資料規格。Relay 來保證這份資料在 render 之前可以使用。
一開始,讓我們來建構一個一般 React 版本的 <ProfilePicture>
component,它顯示 user 的 profile photo 和一個調整 photo 的 size 用的 slider。
這是一個 <ProfilePicture>
的基本實作,它為了聚焦在功能而忽略了樣式:
class ProfilePicture extends React.Component { render() { // 預期 `user` prop 會有以下的形狀: // { // profilePhoto: { // uri, // } // } var user = this.props.user; return ( <View> <Image uri={user.profilePhoto.uri} width={...} /> <Slider onChange={value => this.setSize(value)} /> </View> ); } // 更新 photo 的 size setSize(photoSize) { // 待完成:針對給定的 size 抓取 profile photo 的 URI ... } }
在 Relay 中,使用 GraphQL 來描述資料依賴關係。以 <ProfilePicture>
來說,依賴關係可以像下面那樣表示。注意,這正好符合這個 component 預期的 user
prop 形狀。
Relay.QL` # 這個 fragment 僅適用於 type 'User' 的物件。 fragment on User { # 把 'size' 參數設定成一個命名成 '$size' 的 GraphQL 變數,如此以來我們可以 # 在之後藉由 slider 改變它的值。 profilePhoto(size: $size) { # 針對指定的 size 取得相應的 URI,例如在 CDN 上。 uri, }, } `
已經有了一般的 React component 和一個 GraphQL fragment,我們現在可以定義一個 Container
來告訴 Relay 這個 component 的資料需求。讓我們先看一下程式碼並接著看看會發生什麼事:
class ProfilePicture extends React.Component {/* 同上 */} // Export 一個包裝了原始 `<ProfilePicture>` 的*新* React component。 module.exports = Relay.createContainer(ProfilePicture, { // 指定 `$size` 變數的初始值。 initialVariables: { size: 32 }, // 我們為每一個倚靠伺服器資料的 props,在 `fragments` 裡 // 定義一個對應的 key。在這裡,這個 component 預期伺服器的資料會填進 // `user` prop,因此我們把上面的 fragment 指定為 `fragments.user`。 fragments: { user: () => Relay.QL` fragment on User { profilePhoto(size: $size) { uri, }, } `, }, });
Relay container 是 higher-order component — Relay.createContainer
是一個接收一個 React component 作為 input 並回傳一個新的 component 作為 output 的 function。這意味著這些 container 可以管理資料抓取和解析的邏輯而不用干擾內部 component 的 state
。
下面是當 container 被 render 時會發生的事情:
在上面的圖中:
User
「record」的參考被傳遞進去 parent component。Relay(ProfilePicture)
以便於除錯 — 將會從本地的 store 為每一個 GraphQL fragment 取得回應。<ProfilePicture>
component。<ProfilePicture>
接收一個是一般 JavaScript 資料的 user
prop在上面的範例還剩下一件事沒做 — 實作 setSize()
,它應該要在 slider 的值改變時改變 photo 的 size。除了把每一個 query 的結果傳遞到 component 之外,Relay 也提供一個 relay
prop,它有 Relay 專用的方法和 metadata。這包括 variables
— 用來抓取當下 props
的變數 — 以及 setVariables()
— 一個可以被用來請求不同變數值的資料的 callback。
class ProfilePicture extends React.Component { render() { // 存取用 `user` fragment 解析出來的資料。 var user = this.props.user; // 存取當下被用來抓取 `user` 的 `variables`。 var variables = this.props.relay.variables; return ( <View> <Image uri={user.profilePhoto.uri} width={variables.size} /> <Slider onChange={value => this.setSize(value)} /> </View> ); } // 更新 photo 的 size。 setSize(photoSize) { // `setVariables()` 告訴 Relay 這個 component 的資料需求已經 // 改變。`props.relay.variables` 和 `props.user` 的值會 // 持續反映它們先前的值直到新變數的資料 // 已經從伺服器抓取回來。一旦新變數的資料可以使用 // ,這個 component 會立刻用更新後的 // `user` prop 和 `variables.size` 重新 render。 this.props.relay.setVariables({ size: photoSize, }); } }
React 和 Relay 透過合成來支援建立任意複雜的應用程式。可以藉由合成比較小的 component 來建立比較大的 component,幫助我們建立模組化、強健的應用程式。在 Relay 中,合成 component 有兩個方面:
讓我們藉由組合了上面的 <ProfilePicture>
的 <Profile>
component 來探索這是如何運作的。
View 合成就是你已經習慣的東西 — Relay container 是標準的 React component。以下是 <Profile>
component:
class Profile extends React.Component { render() { // 預期一個有 `name` 字串的 `user`,以及要給 `<ProfilePicture>` // 的資訊 (我們會在接下來取得它)。 var user = this.props.user; return ( <View> {/* 它就像一個 React component 一般運作,因為它就是一個 React component! */} <ProfilePicture user={user} /> <Text>{user.name}</Text> </View> ); } }
Fragment 的合成也很類似 — parent container 的 fragment 合成了它每一個 children 的 fragment。在這個範例中,<Profile>
需要抓取 <ProfilePicture>
需要的 User
相關資訊。
Relay container 提供一個 static 的 getFragment()
方法,它回傳一個指向 component 的 fragment 的參考:
class Profile extends React.Component {/* 同上 */} module.exports = Relay.createContainer(Profile, { <<<<<<< HEAD fragments: { // 這個 `user` fragment 名稱對應到命名成 prop `user` 的 prop, // 這預計會藉由 `<Profile>` component 填入伺服器的資料。 user: () => Relay.QL` fragment on User { # 指定 '<Profile>' 本身需要的任何欄位。 name, # 包含一個指向 child component 的 fragment 的參考。在這裡, # 'user' 是被指定在 child '<ProfilePicture>' 的 'fragments' 定義中 # 的 fragment 名稱。 ${ProfilePicture.getFragment('user')}, } `, } });
這最後的資料宣告等同於以下的一般 GraphQL:
`
fragment Profile on User {
name,
...ProfilePhoto,
}
fragment ProfilePhoto on User {
profilePhoto(size: $size) {
uri,
},
}
`
注意,在合成 fragment 時,被合成的 fragment 的 type 必須符合嵌入它的 parent 上的欄位。例如,把一個 type Story
的 fragment 嵌入 parent 的一個 type User
的欄位並沒有意義。Relay 和 GraphQL 會提供有幫助的錯誤訊息,如果你犯了這個錯誤 (而且錯誤訊息沒有幫助,請讓我們知道!)。
正如我們已經學過的,Relay container 把資料需求宣告成 GraphQL fragment。這意味著,舉例來說,<ProfilePicture>
不只是可以被嵌在 <Profile>
裡,也可以嵌在任何有抓取 type User
欄位的 container。
我們差不多準備好要讓 Relay 滿足這些 component 的資料需求並 render 它們了。不過,有一個問題。要實際的使用 GraphQL 抓取資料,我們需要一個 query root。例如,我們需要把 <Profile>
fragment 建立在一個具體的 type User
的 node 上。
在 Relay 中,一個 query 的 root 是由 Route 來定義。請繼續學習有關 Relay 的 routes。