rescript-relay
repository·master·Indexed 43 days ago
https://github.com/zth/rescript-relayA ReScript binding for Relay that provides type-safe, autogenerated hooks for GraphQL fragments and queries. It includes a PPX for the %relay() syntax macro, a dedicated VSCode extension, and a CLI for managing generated artifacts.
What's inside rescript-relay
- RescriptRelay is a bridge that allows you to use Relay—a JavaScript client-side framework for building data-driven applications using GraphQL—within a ReScript environment. It provides a way to leverage Relay's powerful data-fetching capabilities while maintaining ReScript's type safety and developer experience.
Explore the tutorial project structure
masterThe example newsfeed application is organized into the following directory structure:
src/components: Contains the React components.App.res: The top-level component.Newsfeed.res: Fetches newsfeed stories and displays a scrolling list (initially uses hard-coded data).Story.res: Displays an individual newsfeed story.
server: Contains the backend logic.server/schema.graphql: The GraphQL schema defining the data available for querying.
Learn about Relay suspense and partial rendering
masterUnderstand how to manage complex loading states and UI transitions using:
- Suspense: When and why Relay suspends, and how to control loading states and route transitions using React Suspense and
RescriptRelayRouter. - Partial Rendering: How to render views using existing cached data.
- Developer Tools: Using Relay and React browser dev tools to debug suspense and data flow.
- Suspense: When and why Relay suspends, and how to control loading states and route transitions using React Suspense and
Understand the relationship between GraphQL and Relay
masterGraphQL is a query language for APIs that allows clients to request specific data shapes from a server, reducing over-fetching and under-fetching. Instead of fixed endpoints, the server provides a schema defining nodes (objects/records) and edges (connections).
Relay is a data management library designed specifically for GraphQL. It solves the problem of data fetching by allowing individual components to declare their own data requirements using Fragments. A Relay Compiler then scans your code, stitches these fragments together into efficient, top-level queries, and deduplicates requests for the same data. This provides a balance of high performance (single request per screen) and high maintainability (components are decoupled from the screens they inhabit).
Anatomy of a GraphQL Mutation
masterA GraphQL mutation functions similarly to a Query but is intended to cause side effects on the server. A mutation consists of:
- Variables: Declared in the mutation signature (e.g.,
$id: ID!) to allow the client to pass dynamic data to the server. - Mutation Fields: Specific fields implemented by the server (e.g.,
likeStory) that correspond to actions. These fields accept arguments which are mapped to the mutation's variables. - Response Selection: After the action is performed, the mutation can select fields to return data. This returned data is used to update the local Relay store.
- Variables: Declared in the mutation signature (e.g.,
Choose the right invalidation strategy
masterSelect an invalidation method based on the scope of the change:
- Individual record invalidation (
RecordProxy.invalidateRecord): Use when you know exactly which single entity changed and want to keep other cached data valid. - Bulk record invalidation (
invalidateRecordsByIds): Use for multiple related records or connection instances (e.g., after deleting a post, invalidate all its comment connections). - Store-wide invalidation (
RecordSourceSelectorProxy.invalidateStore): Use for major app-wide changes like user logout or authentication state changes where you want to clear the entire cache.
- Individual record invalidation (
How fragments in fragments and data masking work
masterRelay supports nesting fragments by spreading one fragment inside another. This allows components to compose their data requirements without the top-level query needing to know the internal details of the sub-components.
This behavior implements data masking: a component can only access the specific fields defined in its own fragment. Even if a parent query fetches extra data, that data is not available to the component unless the component explicitly requests it via its own fragment.
/* UserProfileHeader.res */ module UserFragment = %relay( ` fragment UserProfileHeader_user on User { firstName lastName ...Avatar_user } ` ) @react.component let make = (~user) => { let user = UserFragment.use(user) <div> <Avatar user=user.fragmentRefs /> {React.string(user.firstName ++ " " ++ user.lastName)} </div> }Use the @alias directive for conditional fragments
masterBy default, all fragment references on an object are nested under a single
fragmentRefsproperty. While convenient, this makes it difficult to handle conditional fragments (e.g.,@include(if: $variable)) or abstract types.Using the
@aliasdirective places each fragment on its own unique, optional property on the resulting record. This allows you to check for the presence of a fragment or access it directly via a named property, which is safer for conditional logic./* UserProfileHeader.res */ module UserFragment = %relay( ` fragment UserProfileHeader_user on User { firstName lastName ...Avatar_user @alias } ` ) @react.component let make = (~user) => { let user = UserFragment.use(user) <div> <Avatar user=user.avatar_user /> {React.string(user.firstName ++ " " ++ user.lastName)} </div> }Generate type-safe connection IDs with makeConnectionId
masterWhen working with
@connectiondirectives, you often need the connection's__idto perform cache updates. If passing__idthrough the React tree is difficult, RescriptRelay generates amakeConnectionIdfunction for every fragment containing a@connection.This function is fully type-safe and mirrors the
@connectionand@argumentDefinitions(including default values) defined in your GraphQL.Usage Pattern:
- Pass the
__idof the owner (the type that owns the connection) tomakeConnectionId. - Provide the necessary arguments. If an argument is omitted, the function uses the
defaultValuespecified in the@argumentDefinitionswithin the GraphQL text. - Use the resulting ID for declarative updates or imperative store operations via
RecordSourceSelectorProxy.get.
/* UserFriendsList.res */ module Fragment = %relay(` fragment UserFriendsList_user on User @argumentDefinitions( onlineStatuses: { type: "[OnlineStatus!]", defaultValue: [Idle] } count: { type: "Int", defaultValue: 2 } cursor: { type: "String", defaultValue: "" } minimumCommonFriends: { type: "Int!" } ) { __id friendsConnection( statuses: $onlineStatuses first: $count after: $cursor minimumCommonFriends: $minimumCommonFriends ) @connection(key: "UserFriendsList_user_friendsConnection") { edges { node { id ...UserFriendsListSingleUser_user } } } } `) @react.component let make = (~user) => { let user = Fragment.use(user) // Uses default onlineStatuses: [Idle] let connectionId = user.__id->UserFriendsList_user_graphql.makeConnectionId(~minimumCommonFriends=10) // Uses explicit onlineStatuses let connectionId2 = user.__id->UserFriendsList_user_graphql.makeConnectionId( ~minimumCommonFriends=10, ~onlineStatuses=[#Online, #Idle] ) // ... }- Pass the
Preload queries using useLoader and usePreloaded
masterTo start loading data before a component renders (to avoid waterfalls), use the preloading pattern. This involves two steps:
useLoader: Call this to start the loading process. It returns a tuple:(option(queryRef), loadQuery, disposeQuery).loadQueryis the function you call to trigger the fetch.queryRefis the reference to the loading query.
usePreloaded: Pass thequeryRefobtained fromuseLoaderinto this hook. It will either deliver the data immediately or suspend the component until the data arrives.
This pattern uses Relay's
useQueryLoaderandusePreloadedQueryhooks.// 1. Initialize the loader let (queryRef, loadQuery, disposeQuery) = Query.useLoader() // 2. Trigger the load (e.g., in a useEffect or event handler) loadQuery(~variables=()) // 3. In a child component or later in the render, consume the ref let data = Query.usePreloaded(~queryRef=queryRef)GraphQL Schema Fundamentals: Nodes, Fields, and Types
masterA GraphQL schema defines the structure of your data. Key concepts include:
- Nodes: The objects or records in your graph (e.g.,
Person,Story). - Fields: The properties of a node. Fields can be:
- Scalars: Primitive types like
String,Int,ID, orBoolean. - Objects: Fields that point to other nodes (edges).
- Scalars: Primitive types like
- Non-nullable fields (
!): A type suffix (e.g.,ID!) indicating that the field cannot be null. If a query requests a non-nullable field that is null at runtime, it results in an error. - Interfaces: A way to define a set of fields that multiple types can implement (e.g., an
Actorinterface implemented byPersonandOrganization). - Enums: A set of predefined constant values (e.g.,
Category { EDUCATION, NEWS, COOKING }). - Top-level entry points:
Query: Used for fetching data (analogous to HTTP GET).Mutation: Used for modifying data (analogous to HTTP POST).Subscription: Used for real-time updates.
- Nodes: The objects or records in your graph (e.g.,
Preload queries using the generated `load` function
masterFor maximum performance, especially when integrated with a router, you can bypass the React render cycle by calling the
loadfunction directly from the generated GraphQL artifact file.When you define a query using
%relay, the compiler generates a file named<queryName>_graphql.res. This file contains aloadfunction that returns aqueryRef. You can call this function anywhere (e.g., during route preparation) and then pass the resultingqueryRefto a component usingQuery.usePreloaded./* Example using rescript-relay-router to preload data during route preparation */ module SomeComponent = %relay.deferredComponent(SomeComponent.make) let renderer = Routes.SomeRoute.Route.makeRenderer( ~prepareCode=_ => [SomeComponentQuery.preload()], ~prepare=({environment, userId}) => { SomeComponentQuery_graphql.load( ~environment, ~variables={userId: userId}, ~fetchPolicy=StoreOrNetwork, ) }, ~render=props => { <SomeComponent queryRef=props.prepared /> }, )