|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105 |
- title: Thinking in GraphQL
- url: https://facebook.github.io/relay/docs/thinking-in-graphql.html
- hash_url: db7ba122f32c64df26c075bdbbee18fb
-
- <p>GraphQL presents new ways for clients to fetch data by focusing on the needs of product developers and client applications. It provides a way for developers to specify the precise data needed for a view and enables a client to fetch that data in a single network request. Compared to traditional approaches such as REST, GraphQL helps applications to fetch data more efficiently (compared to resource-oriented REST approaches) and avoid duplication of server logic (which can occur with custom endpoints). Furthermore, GraphQL helps developers to decouple product code and server logic. For example, a product can fetch more or less information without requiring a change to every relevant server endpoint. It's a great way to fetch data.</p><p>In this article we'll explore what it means to build a GraphQL client framework and how this compares to clients for more traditional REST systems. Along the way we'll look at the design decisions behind Relay and see that it's not just a GraphQL client but also a framework for <em>declarative data-fetching</em>. Let's start at the beginning and fetch some data!</p><h2>Fetching Data <a class="hash-link" href="#fetching-data">#</a></h2><p>Imagine we have a simple application that fetches a list of stories, and some details about each one. Here's how that might look in resource-oriented REST:</p><pre class="prism language-javascript">
- rest<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span><span class="token string">'/stories'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span>stories <span class="token operator">=</span><span class="token operator">></span>
-
-
- Promise<span class="token punctuation">.</span><span class="token function">all</span><span class="token punctuation">(</span>stories<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>story <span class="token operator">=</span><span class="token operator">></span>
- rest<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span>story<span class="token punctuation">.</span>href<span class="token punctuation">)</span>
- <span class="token punctuation">)</span><span class="token punctuation">)</span>
- <span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span>stories <span class="token operator">=</span><span class="token operator">></span> <span class="token punctuation">{</span>
-
-
- console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>stories<span class="token punctuation">)</span><span class="token punctuation">;</span>
- <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></pre><p>Note that this approach requires <em>n+1</em> requests to the server: 1 to fetch the list, and <em>n</em> to fetch each item. With GraphQL we can fetch the same data in a single network request to the server (without creating a custom endpoint that we'd then have to maintain):</p><pre class="prism language-javascript">graphql<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span><span class="token template-string"><span class="token string">`query { stories { id, text } }`</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span>
- stories <span class="token operator">=</span><span class="token operator">></span> <span class="token punctuation">{</span>
-
-
- console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>stories<span class="token punctuation">)</span><span class="token punctuation">;</span>
- <span class="token punctuation">}</span>
- <span class="token punctuation">)</span><span class="token punctuation">;</span></pre><p>So far we're just using GraphQL as a more efficient version of typical REST approaches. Note two important benefits in the GraphQL version:</p><ul><li>All data is fetched in a single round trip.</li><li>The client and server are decoupled: the client specifies the data needed instead of <em>relying on</em> the server endpoint to return the correct data.</li></ul><p>For a simple application that's already a nice improvement.</p><h2>Client Caching <a class="hash-link" href="#client-caching">#</a></h2><p>Repeatedly refetching information from the server can get quite slow. For example, navigating from the list of stories, to a list item, and back to the list of stories means we have to refetch the whole list. We'll solve this with the standard solution: <em>caching</em>.</p><p>In a resource-oriented REST system, we can maintain a <strong>response cache</strong> based on URIs:</p><pre class="prism language-javascript"><span class="token keyword">var</span> _cache <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Map</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
- rest<span class="token punctuation">.</span><span class="token keyword">get</span> <span class="token operator">=</span> uri <span class="token operator">=</span><span class="token operator">></span> <span class="token punctuation">{</span>
- <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>_cache<span class="token punctuation">.</span><span class="token function">has</span><span class="token punctuation">(</span>uri<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
- _cache<span class="token punctuation">.</span><span class="token keyword">set</span><span class="token punctuation">(</span>uri<span class="token punctuation">,</span> <span class="token function">fetch</span><span class="token punctuation">(</span>uri<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
- <span class="token punctuation">}</span>
- <span class="token keyword">return</span> _cache<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span>uri<span class="token punctuation">)</span><span class="token punctuation">;</span>
- <span class="token punctuation">}</span><span class="token punctuation">;</span></pre><p>Response-caching can also be applied to GraphQL. A basic approach would work similarly to the REST version. The text of the query itself can be used as a cache key:</p><pre class="prism language-javascript"><span class="token keyword">var</span> _cache <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Map</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
- graphql<span class="token punctuation">.</span><span class="token keyword">get</span> <span class="token operator">=</span> queryText <span class="token operator">=</span><span class="token operator">></span> <span class="token punctuation">{</span>
- <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>_cache<span class="token punctuation">.</span><span class="token function">has</span><span class="token punctuation">(</span>queryText<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
- _cache<span class="token punctuation">.</span><span class="token keyword">set</span><span class="token punctuation">(</span>queryText<span class="token punctuation">,</span> <span class="token function">fetchGraphQL</span><span class="token punctuation">(</span>queryText<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
- <span class="token punctuation">}</span>
- <span class="token keyword">return</span> _cache<span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">(</span>queryText<span class="token punctuation">)</span><span class="token punctuation">;</span>
- <span class="token punctuation">}</span><span class="token punctuation">;</span></pre><p>Now, requests for previously cached data can be answered immediately without making a network request. This is a practical approach to improving the perceived performance of an application. However, this method of caching can cause problems with data consistency.</p><h2>Cache Consistency <a class="hash-link" href="#cache-consistency">#</a></h2><p>With GraphQL it is very common for the results of multiple queries to overlap. However, our response cache from the previous section doesn't account for this overlap — it caches based on distinct queries. For example, if we issue a query to fetch stories:</p><pre class="prism language-javascript">query <span class="token punctuation">{</span> stories <span class="token punctuation">{</span> id<span class="token punctuation">,</span> text<span class="token punctuation">,</span> likeCount <span class="token punctuation">}</span> <span class="token punctuation">}</span></pre><p>and then later refetch one of the stories whose <code>likeCount</code> has since been incremented:</p><pre class="prism language-javascript">query <span class="token punctuation">{</span> <span class="token function">story</span><span class="token punctuation">(</span>id<span class="token punctuation">:</span> <span class="token string">"123"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> id<span class="token punctuation">,</span> text<span class="token punctuation">,</span> likeCount <span class="token punctuation">}</span> <span class="token punctuation">}</span></pre><p>We'll now see different <code>likeCount</code>s depending on how the story is accessed. A view that uses the first query will see an outdated count, while a view using the second query will see the updated count.</p><h3>Caching A Graph <a class="hash-link" href="#caching-a-graph">#</a></h3><p>The solution to caching GraphQL is to normalize the hierarchical response into a flat collection of <strong>records</strong>. Relay implements this cache as a map from IDs to records. Each record is a map from field names to field values. Records may also link to other records (allowing it to describe a cyclic graph), and these links are stored as a special value type that references back into the top-level map. With this approach each server record is stored <em>once</em> regardless of how it is fetched.</p><p>Here's an example query that fetches a story's text and its author's name:</p><pre class="prism language-javascript">query <span class="token punctuation">{</span>
- <span class="token function">story</span><span class="token punctuation">(</span>id<span class="token punctuation">:</span> <span class="token string">"1"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
- text<span class="token punctuation">,</span>
- author <span class="token punctuation">{</span>
- name
- <span class="token punctuation">}</span>
- <span class="token punctuation">}</span>
- <span class="token punctuation">}</span></pre><p>And here's a possible response:</p><pre class="prism language-javascript">query<span class="token punctuation">:</span> <span class="token punctuation">{</span>
- story<span class="token punctuation">:</span> <span class="token punctuation">{</span>
- text<span class="token punctuation">:</span> <span class="token string">"Relay is open-source!"</span><span class="token punctuation">,</span>
- author<span class="token punctuation">:</span> <span class="token punctuation">{</span>
- name<span class="token punctuation">:</span> <span class="token string">"Jan"</span>
- <span class="token punctuation">}</span>
- <span class="token punctuation">}</span>
- <span class="token punctuation">}</span></pre><p>Although the response is hierarchical, we'll cache it by flattening all the records. Here is an example of how Relay would cache this query response:</p><pre class="prism language-javascript">Map <span class="token punctuation">{</span>
-
- <span class="token number">1</span><span class="token punctuation">:</span> Map <span class="token punctuation">{</span>
- text<span class="token punctuation">:</span> <span class="token string">'Relay is open-source!'</span><span class="token punctuation">,</span>
- author<span class="token punctuation">:</span> <span class="token function">Link</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
- <span class="token punctuation">}</span><span class="token punctuation">,</span>
-
- <span class="token number">2</span><span class="token punctuation">:</span> Map <span class="token punctuation">{</span>
- name<span class="token punctuation">:</span> <span class="token string">'Jan'</span><span class="token punctuation">,</span>
- <span class="token punctuation">}</span><span class="token punctuation">,</span>
- <span class="token punctuation">}</span><span class="token punctuation">;</span></pre><p>This is only a simple example: in reality the cache must handle one-to-many associations and pagination (among other things).</p><h3>Using The Cache <a class="hash-link" href="#using-the-cache">#</a></h3><p>So how do we use this cache? Let's look at two operations: writing to the cache when a response is received, and reading from the cache to determine if a query can be fulfilled locally (the equivalent to <code>_cache.has(key)</code> above, but for a graph).</p><h3>Populating The Cache <a class="hash-link" href="#populating-the-cache">#</a></h3><p>Populating the cache involves walking a hierarchical GraphQL response and creating or updating normalized cache records. At first it may seem that the response alone is sufficient to process the response, but in fact this is only true for very simple queries. Consider <code>user(id: "456") { photo(size: 32) { uri } }</code> — how should we store <code>photo</code>? Using <code>photo</code> as the field name in the cache won't work because a different query might fetch the same field but with different argument values (e.g. <code>photo(size: 64) {...}</code>). A similar issue occurs with pagination. If we fetch the 11th to 20th stories with <code>stories(first: 10, offset: 10)</code>, these new results should be <em>appended</em> to the existing list.</p><p>Therefore, a normalized response cache for GraphQL requires processing payloads and queries in parallel. For example, the <code>photo</code> field from above might be cached with a generated field name such as <code>photo_size(32)</code> in order to uniquely identify the field and its argument values.</p><h3>Reading From Cache <a class="hash-link" href="#reading-from-cache">#</a></h3><p>To read from the cache we can walk a query and resolve each field. But wait: that sounds <em>exactly</em> like what a GraphQL server does when it processes a query. And it is! Reading from the cache is a special case of an executor where a) there's no need for user-defined field functions because all results come from a fixed data structure and b) results are always synchronous — we either have the data cached or we don't.</p><p>Relay implements several variations of <strong>query traversal</strong>: operations that walk a query alongside some other data such as the cache or a response payload. For example, when a query is fetched Relay performs a "diff" traversal to determine what fields are missing (much like React diffs virtual DOM trees). This can reduce the amount of data fetched in many common cases and even allow Relay to avoid network requests at all when queries are fully cached.</p><h3>Cache Updates <a class="hash-link" href="#cache-updates">#</a></h3><p>Note that this normalized cache structure allows overlapping results to be cached without duplication. Each record is stored once regardless of how it is fetched. Let's return to the earlier example of inconsistent data and see how this cache helps in that scenario.</p><p>The first query was for a list of stories:</p><pre class="prism language-javascript">query <span class="token punctuation">{</span> stories <span class="token punctuation">{</span> id<span class="token punctuation">,</span> text<span class="token punctuation">,</span> likeCount <span class="token punctuation">}</span> <span class="token punctuation">}</span></pre><p>With a normalized response cache, a record would be created for each story in the list. The <code>stories</code> field would store links to each of these records.</p><p>The second query refetched the information for one of those stories:</p><pre class="prism language-javascript">query <span class="token punctuation">{</span> <span class="token function">story</span><span class="token punctuation">(</span>id<span class="token punctuation">:</span> <span class="token string">"123"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> id<span class="token punctuation">,</span> text<span class="token punctuation">,</span> likeCount <span class="token punctuation">}</span> <span class="token punctuation">}</span></pre><p>When this response is normalized, Relay can detect that this result overlaps with existing data based on its <code>id</code>. Rather than create a new record, Relay will update the existing <code>123</code> record. The new <code>likeCount</code> is therefore available to <em>both</em> queries, as well as any other query that might reference this story.</p><h2>Data/View Consistency <a class="hash-link" href="#data-view-consistency">#</a></h2><p>A normalized cache ensures that the <em>cache</em> is consistent. But what about our views? Ideally, our React views would always reflect the current information from the cache.</p><p>Consider rendering the text and comments of a story along with the corresponding author names and photos. Here's the GraphQL query:</p><pre class="prism language-javascript">query <span class="token punctuation">{</span>
- <span class="token function">node</span><span class="token punctuation">(</span>id<span class="token punctuation">:</span> <span class="token string">"1"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
- text<span class="token punctuation">,</span>
- author <span class="token punctuation">{</span> name<span class="token punctuation">,</span> photo <span class="token punctuation">}</span><span class="token punctuation">,</span>
- comments <span class="token punctuation">{</span>
- text<span class="token punctuation">,</span>
- author <span class="token punctuation">{</span> name<span class="token punctuation">,</span> photo <span class="token punctuation">}</span>
- <span class="token punctuation">}</span>
- <span class="token punctuation">}</span>
- <span class="token punctuation">}</span></pre><p>After initially fetching this story our cache might be as follows. Note that the story and comment both link to the same record as <code>author</code>:</p><pre class="prism language-javascript">
-
- Map <span class="token punctuation">{</span>
-
- <span class="token number">1</span><span class="token punctuation">:</span> Map <span class="token punctuation">{</span>
- author<span class="token punctuation">:</span> <span class="token function">Link</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
- comments<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token function">Link</span><span class="token punctuation">(</span><span class="token number">3</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
- <span class="token punctuation">}</span><span class="token punctuation">,</span>
-
- <span class="token number">2</span><span class="token punctuation">:</span> Map <span class="token punctuation">{</span>
- name<span class="token punctuation">:</span> <span class="token string">'Yuzhi'</span><span class="token punctuation">,</span>
- photo<span class="token punctuation">:</span> <span class="token string">'http://.../photo1.jpg'</span><span class="token punctuation">,</span>
- <span class="token punctuation">}</span><span class="token punctuation">,</span>
-
- <span class="token number">3</span><span class="token punctuation">:</span> Map <span class="token punctuation">{</span>
- author<span class="token punctuation">:</span> <span class="token function">Link</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
- <span class="token punctuation">}</span><span class="token punctuation">,</span>
- <span class="token punctuation">}</span></pre><p>The author of this story also commented on it — quite common. Now imagine that some other view fetches new information about the author, and her profile photo has changed to a new URI. Here's the <em>only</em> part of our cached data that changes:</p><pre class="prism language-javascript">Map <span class="token punctuation">{</span>
- <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
- <span class="token number">2</span><span class="token punctuation">:</span> Map <span class="token punctuation">{</span>
- <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
- photo<span class="token punctuation">:</span> <span class="token string">'http://.../photo2.jpg'</span><span class="token punctuation">,</span>
- <span class="token punctuation">}</span><span class="token punctuation">,</span>
- <span class="token punctuation">}</span></pre><p>The value of the <code>photo</code> field has changed; and therefore the record <code>2</code> has also changed. And that's it. Nothing else in the <em>cache</em> is affected. But clearly our <em>view</em> needs to reflect the update: both instances of the author in the UI (as story author and comment author) need to show the new photo.</p><p>A standard response is to "just use immutable data structures" — but let's see what would happen if we did:</p><pre class="prism language-javascript">ImmutableMap <span class="token punctuation">{</span>
- <span class="token number">1</span><span class="token punctuation">:</span> ImmutableMap <span class="token punctuation">{</span><span class="token punctuation">}</span>
- <span class="token number">2</span><span class="token punctuation">:</span> ImmutableMap <span class="token punctuation">{</span>
- <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
- photo<span class="token punctuation">:</span> <span class="token string">'http://.../photo2.jpg'</span><span class="token punctuation">,</span>
- <span class="token punctuation">}</span><span class="token punctuation">,</span>
- <span class="token number">3</span><span class="token punctuation">:</span> ImmutableMap <span class="token punctuation">{</span><span class="token punctuation">}</span>
- <span class="token punctuation">}</span></pre><p>If we replace <code>2</code> with a new immutable record, we'll also get a new immutable instance of the cache object. However, records <code>1</code> and <code>3</code> are untouched. Because the data is normalized, we can't tell that <code>story</code>'s contents have changed just by looking at the <code>story</code> record alone.</p><h3>Achieving View Consistency <a class="hash-link" href="#achieving-view-consistency">#</a></h3><p>There are a variety of solutions for keeping views up to date with a flattened cache. The approach that Relay takes is to maintain a mapping from each UI view to the set of IDs it references. In this case, the story view would subscribe to updates on the story (<code>1</code>), the author (<code>2</code>), and the comments (<code>3</code> and any others). When writing data into the cache, Relay tracks which IDs are affected and notifies <em>only</em> the views that are subscribed to those IDs. The affected views re-render, and unaffected views opt-out of re-rendering for better performance (Relay provides a safe but effective default <code>shouldComponentUpdate</code>). Without this strategy, every view would re-render for even the tiniest change.</p><p>Note that this solution will also work for <em>writes</em>: any update to the cache will notify the affected views, and writes are just another thing that updates the cache.</p><h2>Mutations <a class="hash-link" href="#mutations">#</a></h2><p>So far we've looked at the process of querying data and keeping views up to date, but we haven't looked at writes. In GraphQL, writes are called <strong>mutations</strong>. We can think of them as queries with side effects. Here's an example of calling a mutation that might mark a given story as being liked by the current user:</p><pre class="prism language-javascript">
-
- mutation <span class="token function">StoryLike</span><span class="token punctuation">(</span>$storyID<span class="token punctuation">:</span> String<span class="token punctuation">)</span> <span class="token punctuation">{</span>
-
- <span class="token function">storyLike</span><span class="token punctuation">(</span>storyID<span class="token punctuation">:</span> $storyID<span class="token punctuation">)</span> <span class="token punctuation">{</span>
-
- likeCount
- <span class="token punctuation">}</span>
- <span class="token punctuation">}</span></pre><p>Notice that we're querying for data that <em>may</em> have changed as a result of the mutation. An obvious question is: why can't the server just tell us what changed? The answer is: it's complicated. GraphQL abstracts over <em>any</em> data storage layer (or an aggregation of multiple sources), and works with any programming language. Furthermore, the goal of GraphQL is to provide data in a form that is useful to product developers building a view.</p><p>We've found that it's common for the GraphQL schema to differ slightly or even substantially from the form in which data is stored on disk. Put simply: there isn't always a 1:1 correspondence between data changes in your underlying <em>data storage</em> (disk) and data changes in your <em>product-visible schema</em> (GraphQL). The perfect example of this is privacy: returning a user-facing field such as <code>age</code> might require accessing numerous records in our data-storage layer to determine if the active user is even allowed to <em>see</em> that <code>age</code> (Are we friends? Is my age shared? Did I block you? etc.).</p><p>Given these real-world constraints, the approach in GraphQL is for clients to query for things that may change after a mutation. But what exactly do we put in that query? During the development of Relay we explored several ideas — let's look at them briefly in order to understand why Relay uses the approach that it does:</p><ul><li><p>Option 1: Re-fetch everything that the app has ever queried. Even though only a small subset of this data will actually change, we'll still have to wait for the server to execute the <em>entire</em> query, wait to download the results, and wait to process them again. This is very inefficient.</p></li><li><p>Option 2: Re-fetch only the queries required by actively rendered views. This is a slight improvement over option 1. However, cached data that <em>isn't</em> currently being viewed won't be updated. Unless this data is somehow marked as stale or evicted from the cache subsequent queries will read outdated information.</p></li><li><p>Option 3: Re-fetch a fixed list of fields that <em>may</em> change after the mutation. We'll call this list a <strong>fat query</strong>. We found this to also be inefficient because typical applications only render a subset of the fat query, but this approach would require fetching all of those fields.</p></li><li><p>Option 4 (Relay): Re-fetch the intersection of what may change (the fat query) and the data in the cache. In addition to the cache of data Relay also remembers the queries used to fetch each item. These are called <strong>tracked queries</strong>. By intersecting the tracked and fat queries, Relay can query exactly the set of information the application needs to update and nothing more.</p></li></ul><h2>Data-Fetching APIs <a class="hash-link" href="#data-fetching-apis">#</a></h2><p>So far we looked at the lower-level aspects of data-fetching and saw how various familiar concepts translate to GraphQL. Next, let's step back and look at some higher-level concerns that product developers often face around data-fetching:</p><ul><li>Fetching all the data for a view hierarchy.</li><li>Managing asynchronous state transitions and coordinating concurrent requests.</li><li>Managing errors.</li><li>Retrying failed requests.</li><li>Updating the local cache after receiving query/mutation responses.</li><li>Queuing mutations to avoid race conditions.</li><li>Optimistically updating the UI while waiting for the server to respond to mutations.</li></ul><p>We've found that typical approaches to data-fetching — with imperative APIs — force developers to deal with too much of this non-essential complexity. For example, consider <em>optimistic UI updates</em>. This is a way of giving the user feedback while waiting for a server response. The logic of <em>what</em> to do can be quite clear: when the user clicks "like", mark the story as being liked and send the request to the server. But the implementation is often much more complex. Imperative approaches require us to implement all of those steps: reach into the UI and toggle the button, initiate a network request, retry it if necessary, show an error if it fails (and untoggle the button), etc. The same goes for data-fetching: specifying <em>what</em> data we need often dictates <em>how</em> and <em>when</em> it is fetched. Next, we'll explore our approach to solving these concerns with <strong>Relay</strong>.</p>
|