A Urql exchange for integrating with TinyBase, allowing automatic persistence and synchronization of GraphQL data into a local reactive store.
- Automatic Persistence: Use the
@dbMergeRowdirective to automatically save query and mutation results to TinyBase. - Automatic Deletion: Use the
@dbDeleteRowdirective to remove rows from TinyBase. - Reactive Queries: Seamlessly integrates with TinyBase's reactive components (
useCell,useRow,useQuery). - Advanced Support: Fully compatible with TinyBase Indexes, Metrics, and Queries.
- Works with Queries and Mutations: Sync data from both GraphQL queries and mutations.
npm install urql-tinybase-exchange urql tinybase graphql reactimport { createClient, fetchExchange } from "urql";
import { createStore } from "tinybase";
import { tinyBaseExchange } from "urql-tinybase-exchange";
const store = createStore();
const client = createClient({
url: "http://localhost:4000/graphql",
exchanges: [tinyBaseExchange({ store }), fetchExchange],
});The table argument can be either a string literal or an enum value:
# Define enum in your GraphQL schema (optional but recommended)
enum Table {
Post
Comment
User
Reaction
}
directive @dbMergeRow(table: Table!) on FIELD | FRAGMENT_DEFINITION
directive @dbDeleteRow(table: Table!) on FIELD
# Or use string literals
directive @dbMergeRow(table: String!) on FIELD | FRAGMENT_DEFINITION
directive @dbDeleteRow(table: String!) on FIELDUsing with Queries (Enum or String):
# Using enum (Post will be converted to lowercase "post" for TinyBase table)
fragment PostFragment on Post @dbMergeRow(table: Post) {
id
title
author @dbMergeRow(table: User) {
id
name
}
}
# Or using string literal
fragment PostFragment on Post @dbMergeRow(table: "posts") {
id
title
author @dbMergeRow(table: "users") {
id
name
}
}
query GetPosts {
posts {
...PostFragment
}
}This will automatically sync all posts and their authors to TinyBase when the query returns.
TinyBase table cells store primitive values only (string, number, boolean, null). Nested objects and arrays from your GraphQL response are not retained on the parent row. To persist nested structures, apply @dbMergeRow on the nested object/array fields so they are normalized into their own tables.
- Enum table names are converted to lowercase:
Post→post. If you prefer pluralized or custom names, use string literals like"posts". - Deletion uses field-level directives on the ID(s): apply
@dbDeleteRowdirectly to the field that returns the ID or array of IDs. - Client-only directives are stripped before sending to the backend, so servers won’t see
@dbMergeRow/@dbDeleteRow.
Example with nested fragments and arrays:
fragment UserFragment on User {
id
name
}
fragment ReactionFragment on Reaction {
id
emoji
user @dbMergeRow(table: "users") {
...UserFragment
}
}
fragment CommentFragment on Comment {
id
text
author @dbMergeRow(table: "users") {
...UserFragment
}
reactions @dbMergeRow(table: "reactions") {
...ReactionFragment
}
}
# Using an enum for the parent table; will map to lowercase "post"
fragment PostFragment on Post @dbMergeRow(table: Post) {
id
title
comments @dbMergeRow(table: "comments") {
...CommentFragment
# Nested array of replies, also merged into the "comments" table
replies @dbMergeRow(table: "comments") {
...CommentFragment
}
}
}
mutation CreatePostWithNested {
createPost(id: "p1", title: "Nested") {
...PostFragment
}
}In this example:
- The post is stored in the
posttable (lowercased from enum). Primitive fields likeid,title, and potentially__typenameare stored on the row. comments,replies,author, andreactionsare stored in their own tables via nested@dbMergeRowdirectives.- If you need to keep a JSON blob in a single cell, serialize it (e.g.,
contentJsonas a string), understanding you won’t be able to index/query inside that blob via TinyBase.
Merging Data (@dbMergeRow):
mutation CreateUser {
createUser(id: "1", name: "Alice") @dbMergeRow(table: "users") {
id
name
}
}This will automatically do store.setRow('users', '1', { id: '1', name: 'Alice' }).
Deleting Data (@dbDeleteRow):
mutation DeleteUser {
deleteUser(id: "1") {
id @dbDeleteRow(table: "users")
}
}This will automatically do store.delRow('users', '1'). The directive is applied to the id field which contains the row ID to delete.
You can also delete multiple rows by applying the directive to an array field of IDs:
mutation DeleteUsers {
deleteUsers(ids: ["1", "2"]) {
ids @dbDeleteRow(table: "users")
}
}To use TinyBase hooks like useCell, useRow, or useQuery, you must wrap your app with the Provider from tinybase/ui-react and pass the same store instance you used for the exchange.
import React from "react";
import { createClient, Provider as UrqlProvider, fetchExchange } from "urql";
import { createStore } from "tinybase";
import { Provider as TinyBaseProvider, useCell } from "tinybase/ui-react";
import { tinyBaseExchange } from "urql-tinybase-exchange";
// 1. Create the store
const store = createStore();
// 2. Create the client with the exchange using the SAME store
const client = createClient({
url: "http://localhost:4000/graphql",
exchanges: [tinyBaseExchange({ store }), fetchExchange],
});
const UserProfile = ({ id }) => {
// 4. Use standard TinyBase hooks
const name = useCell("users", id, "name");
return <div>User: {name}</div>;
};
export const App = () => (
// 3. Wrap your app with BOTH providers
<TinyBaseProvider store={store}>
<UrqlProvider value={client}>
<UserProfile id="1" />
</UrqlProvider>
</TinyBaseProvider>
);Since TinyBase is reactive, standard hooks will automatically trigger updates when the exchange modifies the store:
import { useCell } from "tinybase/ui-react";
const UserParams = ({ id }) => {
const name = useCell("users", id, "name");
return <div>User: {name}</div>;
};MIT