Building complex UIs that fetch data efficiently can be a challenge. That's where GraphQL and Relay come in, offering a powerful combination for your React application.
resaerch
Here's why you should consider them:
Reasons for GraphQL:
Flexible data fetching: Say goodbye to over-fetching or under-fetching data. With GraphQL, you specify the exact data you need in each component, leading to cleaner code and faster performance.
Single endpoint: No more juggling multiple REST APIs. GraphQL provides a unified query language for all your data needs, simplifying your backend and frontend interactions.
Strong typing: Get error checking and autocompletion with GraphQL's schema, ensuring data consistency and reducing bugs.
Future-proof: GraphQL's independent nature allows your server to evolve without breaking your frontend, making it adaptable to changing needs.
Why Relay over other clients?
react-relay
Performance: Relay's compiler optimizes queries and data fetching, leading to lightning-fast and scalable React applications.
Declarative approach: Instead of manually managing data, you declare your data requirements in Relay, and it handles the rest. This reduces boilerplate code and improves maintainability.
Type safety: Relay auto generate typescript/flow types for you, which enforces type safety throughout your application, reducing runtime errors and ensuring data integrity.
Automatic data management: Relay takes care of caching, optimistic updates, and conflict resolution, freeing you to focus on building your UI.
Compared to other clients:
Apollo Client: While offering flexibility, Apollo requires more manual data management, potentially sacrificing performance and maintainability in larger apps.
URQL: URQL prioritizes simplicity, but might lack advanced features like Relay's compiler and data prefetching.
Ultimately, the choice depends on your project's needs. If you value performance, type safety, and a declarative approach, Relay and GraphQL are a powerful duo for building scalable and maintainable React applications.
But like everything else, GraphQL and Relay have their own strengths and weaknesses.
Confusing documentation: The relay docs feel like they were written by someone who knew the library so well that they assumed most of us will just know about some of its features , even on my second attempt to rewrite a previous Application I still found them confusing.
Typescript gymnastics: Relay auto generates the types for you without need for graphql-codegen , but you have to pass in the correct generated types to the corresponding hooks to get the type safety , it's not always intuitive and the documentation doesn't properly explain it.
Suspense based data fetching: Suspense is great but it relies on Suspense Boundaries with fallbacks for handling loading state and error boundaries to catch thrown errors , with one fetcher function doing all the data fetching if it throws an error while fetching it makes auto recovering or showing appropriate error UIs difficult as Error boundaries are not supported in server side React and have a clunky clear error method which isn't the best UX
The upfront cost: While Relay is very clever about some common pain points like pagination and cache invalidation , the upfront code you to write can be overwhelming coupled with the confusing documentation features and the manual work required in other GraphQL client can feel like a better compromise here the fragment definition fetching all of a Github viewer's repositories
>some of the code snippets below were AI generated for use as pseudo code , tweaks might be required to get them working
and on mutation you'd have to manually update the cache of nested fields to inject the response from the mutation response
For example, if you have a mutation that adds a new repository to the viewer's list, you can use the update function to insert the new repository into the cache, like this:
Relay auto generates Fragment_name$key and Fragment_name$data types , the Fragment_name$key is what should be passed into the usePaginationFragment hook and the Fragment_name$data is what the actual fragment will be of type of , it's not supposed to be used directly inside the hooks.
Also note the paginated fragment takes in the Fragment_name$key as it's second type parameter unlike the useFragment hook that only accepts one type parameter where we pass in Fragment_name$key
which leads me to another accidental discovery I made while figuring out a way to pass the refs into the fragment components with the correct types , a typescript helper type FragmentRef is exposed by relay
tsx
1 refs?:{
2readonly" $fragmentSpreads":FragmentRefs<
3|"Fragment_name"
4|"Fragment_history"
5>;
6}|null;
Will have a type we can pass into a component that houses the components for Fragment_name and Fragment_history avoiding having to use the any type
Dealing with read only types
Relay will return all query types and read only and this might become a problem if you have a query result that returns an array of
typescript
1typeOneItem={
2id
3name
4createdAt
5}
Normally if you wanted to have an ItemCard component you would simply
tsx
1typeItemList=OneItem[]
2{items.mao((item)=>(
3<ItemCardkey={item.id}item={item}/>
4))}
5typeItemCardItem=ItemList[number]
6
7finction ItemCard({ item }:ItemCardItem){
8return<div>{item.name}</div>;
9}
But indexing with a number is not allowed with readonly arrays in Typescript
typescript
1typeItems= ReadOnlyArray<ItemList>
2// ❌ not allowed
3typeItemCardItem= ItemList[number]
So i made a helper type to convert ReadOnlyArray to Array
Suspense boundaries These are mostly used to wrap components that are doing data fetching , but I kept making the mistake of forgetting them and triggering the global Suspense boundary causing the whole page to flicker when data was loading , Or I would wrap the list instead of the whole component
tsx
1<!-- ❌ -->
2functionSomeList(){
3const{ loading, error, data }=useQuery(SOME_QUERY,{
4 variables:{ first:10},
5})
6return(
7<Suspensefallback={<div>Loading...</div>}>
8<div>This is a data fetching component</div>;
9</Suspense>
10)
11}
12
tsx
1<!-- ✅ -->
2functionParentComponent(){
3return(
4<Suspensefallback={<div>Loading...</div>}>
5<SomeList/>
6</Suspense>
7
8)
9
10}
11functionSomeList(){
12const{ loading, error, data }=useQuery(SOME_QUERY,{
13 variables:{ first:10},
14})
15return(
16<div>This is a data fetching component</div>;
17)
18}
19
Skipping the suspense fallback with useTransition I had a search component which would make a bunch or request while one is typing which would trigger the suspense boundary of the parent component covering the whole page the search box included ,one possible work around could have been to hoist the input and the associated useState and pass in the current keyword to the SearchResults component which would also house the data fetching logic and wrap that with a suspense boundary . Or we could wrap the setState with a startTransition to mark the key inputs as more important and render everything else in the background and show the results when ready without a suspense boundary.
tsx
1const[, startTransition]=useTransition();
2const[keyword, setKeyword]=useState("");
3
4const{ loading, error, data }=useQuery(SOME_QUERY,{
5 variables:{ query: keyword, first:19},
6
7})
8
9return(
10<div>
11 <input value={keyword}
12 <!-- ❌ will cause flickers
13 onChange={(e)=>{
14setKeyword(e.target.value)
15
16}} -->
17 <!-- ✅ -->
18 onChange={(e)=>{
19startTransition(()=>{
20setKeyword(e.target.value)
21})
22}}
23 />
24
25<Suspensefallback={<div>Loading...</div>}>
26<SearchResultsdata={data}/>
27</Suspense>
28</div>
29)
30
31
As an addition i also relied on the URL and serach params to store the variables , makes shring URls eas and state is still maitatined after a refresh
It's still awesome though: With all that said relay is still awesome , so awesome it inspired the React server components and the best way to do GraphQL in react