rescript-relay

repository·master·Indexed 43 days ago

https://github.com/zth/rescript-relay

A 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.

Tokens
85.5K
Snippets
252
Records
359
Agent score
55%

What's inside rescript-relay

  1. Explore the tutorial project structure

    master

    The 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.
  2. Learn about Relay suspense and partial rendering

    master

    Understand 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.
  3. Understand the relationship between GraphQL and Relay

    master

    GraphQL 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).

  4. Anatomy of a GraphQL Mutation

    master

    A 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.
  5. Choose the right invalidation strategy

    master

    Select an invalidation method based on the scope of the change:

    1. Individual record invalidation (RecordProxy.invalidateRecord): Use when you know exactly which single entity changed and want to keep other cached data valid.
    2. Bulk record invalidation (invalidateRecordsByIds): Use for multiple related records or connection instances (e.g., after deleting a post, invalidate all its comment connections).
    3. 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.
  6. How fragments in fragments and data masking work

    master

    Relay 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>
    }
  7. Use the @alias directive for conditional fragments

    master

    By default, all fragment references on an object are nested under a single fragmentRefs property. While convenient, this makes it difficult to handle conditional fragments (e.g., @include(if: $variable)) or abstract types.

    Using the @alias directive 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>
    }
  8. Generate type-safe connection IDs with makeConnectionId

    master

    When working with @connection directives, you often need the connection's __id to perform cache updates. If passing __id through the React tree is difficult, RescriptRelay generates a makeConnectionId function for every fragment containing a @connection.

    This function is fully type-safe and mirrors the @connection and @argumentDefinitions (including default values) defined in your GraphQL.

    Usage Pattern:

    1. Pass the __id of the owner (the type that owns the connection) to makeConnectionId.
    2. Provide the necessary arguments. If an argument is omitted, the function uses the defaultValue specified in the @argumentDefinitions within the GraphQL text.
    3. 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]
        )
    
      // ...
    }
  9. Preload queries using useLoader and usePreloaded

    master

    To start loading data before a component renders (to avoid waterfalls), use the preloading pattern. This involves two steps:

    1. useLoader: Call this to start the loading process. It returns a tuple: (option(queryRef), loadQuery, disposeQuery).
      • loadQuery is the function you call to trigger the fetch.
      • queryRef is the reference to the loading query.
    2. usePreloaded: Pass the queryRef obtained from useLoader into this hook. It will either deliver the data immediately or suspend the component until the data arrives.

    This pattern uses Relay's useQueryLoader and usePreloadedQuery hooks.

    // 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)
  10. GraphQL Schema Fundamentals: Nodes, Fields, and Types

    master

    A 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, or Boolean.
      • Objects: Fields that point to other nodes (edges).
    • 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 Actor interface implemented by Person and Organization).
    • 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.
  11. Preload queries using the generated `load` function

    master

    For maximum performance, especially when integrated with a router, you can bypass the React render cycle by calling the load function 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 a load function that returns a queryRef. You can call this function anywhere (e.g., during route preparation) and then pass the resulting queryRef to a component using Query.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 />
      },
    )