fate: Bridging Relay's Power with tRPC's Type Safety for Modern React
Share this article
fate: Bridging Relay's Power with tRPC's Type Safety for Modern React
In the ever-evolving landscape of React development, data fetching and state management remain persistent challenges. Christoph Nakazawa, a former member of the original Relay and React teams at Facebook, has unveiled fate, a new data client designed to bring the best of both worlds—Relay's compositional approach and tRPC's type safety—to modern React applications.
The Evolution of Data Fetching
"GraphQL and Relay introduced several novel ideas: fragments co-located with components, a normalized cache keyed by global identifiers, and a compiler that hoists fragments into a single network request," Nakazawa explains. "These innovations made it possible to build large applications where data requirements are modular and self-contained."
Despite these advancements, Nakazawa observed that many React applications still struggle with complex data fetching patterns. "In recent months I have been exposed to the reality of React data fetching. It tends to look something like this:"
const { data: post, isLoading, isError } = useFetch(…)
if (isLoading) return <Loading />;
if (isError) return <Error />;
if (!post) return <NullState />;
return <Post post={post} />;
While this boilerplate is manageable, the complexity escalates significantly when mutations enter the picture:
mutate({
// Complex cache patching logic or detailed cache clearing calls.
onSuccess: (list, newItem) => {
cache.remove('posts');
cache.remove('post', newItem.id);
return cache
.get('root-posts-list')
.map((item) => (item.id === newItem.id ? newItem : item));
},
// Complex rollback logic.
onError: () => {
cache.restore(previousCacheState);
cache
.get('root-posts-list')
.map((item) => (item.id === oldItem.id ? oldItem : item));
},
});
Introducing fate
fate aims to address these challenges by combining Relay's powerful concepts with tRPC's type safety. "fate takes the great ideas from Relay and puts them on top of tRPC," Nakazawa states. "You get the best of both worlds: type safety between the client and server, and GraphQL-like ergonomics for data fetching."
Traditional fetch-based APIs create complexity by caching data based on requests, forcing developers to think about when to fetch data at every component level. "This leads to boilerplate, complexity, and inconsistency," Nakazawa notes. "Instead, fate caches data by objects, shifts thinking to what data is required, and composes data requirements up to a single request at the root."
How fate Works
fate's API is intentionally minimal, focusing on answering the question: "Can we make development easier?" It achieves this through three core concepts: Views, Requests, and Actions.
Views
Views declare data requirements co-located with components:
import type { Post } from '@org/server/views.ts';
import { UserView } from './UserCard.tsx';
import { useView, view, ViewRef } from 'react-fate';
export const PostView = view<Post>()({
author: UserView,
content: true,
id: true,
title: true,
});
export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
const post = useView(PostView, postRef);
return (
<Card>
<h2>{post.title}</h2>
<p>{post.content}</p>
<UserCard user={post.author} />
</Card>
);
};
A ViewRef is a reference to a specific object (like a Post with id 7), containing the object's unique ID, type name, and fate-specific metadata.
Requests
Views are composed at the root of the application and fetched in a single request:
import { useRequest } from 'react-fate';
import { PostCard, PostView } from './PostCard.tsx';
export function App() {
const { posts } = useRequest({ posts: { list: PostView } });
return posts.map((post) => <PostCard key={post.id} post={post} />);
}
Actions
For mutations, fate uses React Actions rather than traditional mutation hooks:
const LikeButton = ({ post }) => {
const fate = useFateClient();
const [result, like] = useActionState(fate.actions.post.like, null);
return (
<Button
action={() =>
like({ input: { id: post.id }, optimistic: { likes: post.likes + 1 } })
}
>
{result?.error ? 'Oops!' : 'Like'}
</Button>
);
};
When this action is called, fate automatically updates all views that depend on the likes field of the particular Post object. "If the action fails, fate rolls back the optimistic update automatically and re-renders all affected components," Nakazawa explains.
The Path Forward
fate is currently in alpha and lacks several core features, including garbage collection, a compiler for static view extraction, and reduced backend boilerplate. However, Nakazawa emphasizes that the current implementation isn't tied to tRPC or Prisma specifically, leaving room for future expansion.
"We welcome contributions and ideas to improve fate," Nakazawa invites. "Here are some features we'd like to add: Support for Drizzle, support backends other than tRPC, persistent storage for offline support, implement garbage collection for the cache, better code generation and less type repetition, support for live views and real-time updates via useLiveView and SSE."
Interestingly, Nakazawa reveals that 80% of fate's code was written by OpenAI's Codex, with careful curation by human developers. "If you contribute to fate, we require you to disclose your use of AI tools," he adds.
The Bigger Picture
fate represents an attempt to solve fundamental problems in React data fetching by bringing together the best ideas from different eras of web development. By combining Relay's compositional approach with tRPC's type safety and leveraging modern Async React features, fate offers a compelling alternative to the status quo.
"Looking ahead to a world where AI increasingly writes more of our code and gravitates towards simple, idiomatic APIs, the problem is that request-centric fetch APIs exist at all," Nakazawa reflects. "If fate does not live up to its promise, I hope that Relay's and fate's ideas will impact future data libraries for the better."
For developers interested in exploring fate, Nakazawa has provided a ready-made template and a runnable demo. The fate documentation covers core concepts in detail, offering a comprehensive introduction to this new approach to data fetching in React.
As the React ecosystem continues to evolve, tools like fate that simplify complex patterns while maintaining type safety and composability will likely play an increasingly important role in how developers build applications.