reactive/data-client

Async State Management without the Management. REST, GraphQL, SSE, Websockets

TypeScript

2,038

5,444 commits

updated Oct 12, 2026

See the code

README

Reactive Data Client

The scalable way to build applications with dynamic data.

Declarative resource definitions for REST, GraphQL, Websockets+SSE and more
Performant rendering in React, NextJS, React Native, Expo, Vue

Schema driven. Zero updater functions.

CircleCI Coverage Status Percentage of issues still open bundle size npm version PRs Welcome Agent Skills Chat

๐Ÿ“–Read The Docs ย |ย  ๐ŸGetting Started ย |ย  ๐Ÿค–Agent Skills
๐ŸŽฎ Demos: ย  Todo ย |ย  Github Social ย |ย  NextJS SSR ย |ย  Websockets+SSR

Installation

npm install --save @data-client/react @data-client/rest @data-client/test

For more details, see the Getting Started docs page.

Skills

npx skills add reactive/data-client

Then run skill "data-client-setup"

Usage

Simple TypeScript definition

class User extends Entity {
  id = '';
  username = '';
}

class Article extends Entity {
  id = '';
  title = '';
  body = '';
  author = User.fromJS();
  createdAt = Temporal.Instant.fromEpochMilliseconds(0);

  static schema = {
    author: User,
    createdAt: Temporal.Instant.from,
  };
}

Create collection of API Endpoints

const UserResource = resource({
  path: '/users/:id',
  schema: User,
  optimistic: true,
});

const ArticleResource = resource({
  path: '/articles/:id',
  schema: Article,
  searchParams: {} as { author?: string },
  optimistic: true,
  paginationField: 'cursor',
});

One line data binding

const article = useSuspense(ArticleResource.get, { id });
return (
  <article>
    <h2>
      {article.title} by {article.author.username}
    </h2>
    <p>{article.body}</p>
  </article>
);

Reactive Mutations

const ctrl = useController();
return (
  <>
    <CreateArticleForm
      onSubmit={article =>
        ctrl.fetch(ArticleResource.getList.push, article)
      }
    />
    <ProfileForm
      onSubmit={user =>
        ctrl.fetch(UserResource.update, { id: article.author.id }, user)
      }
    />
    <button onClick={() => ctrl.fetch(ArticleResource.delete, { id })}>
      Delete
    </button>
  </>
);

Subscriptions

const price = useLive(PriceResource.get, { symbol });
return price.value;

Type-safe Imperative Actions

const ctrl = useController();
await ctrl.fetch(ArticleResource.update, { id }, articleData);
await ctrl.fetchIfStale(ArticleResource.get, { id });
ctrl.expireAll(ArticleResource.getList);
ctrl.invalidate(ArticleResource.get, { id });
ctrl.invalidateAll(ArticleResource.getList);
ctrl.setResponse(ArticleResource.get, { id }, articleData);
ctrl.set(Article, { id }, articleData);

Programmatic queries

const queryTotalVotes = new Query(
  new Collection([BlogPost]),
  posts => posts.reduce((total, post) => total + post.votes, 0),
);

const totalVotes = useQuery(queryTotalVotes);
const totalVotesForUser = useQuery(queryTotalVotes, { userId });
const groupTodoByUser = new Query(
  TodoResource.getList.schema,
  todos => Object.groupBy(todos, todo => todo.userId),
);
const todosByUser = useQuery(groupTodoByUser);

Powerful Middlewares

class LoggingManager implements Manager {
  middleware: Middleware = controller => next => async action => {
    console.log('before', action, controller.getState());
    await next(action);
    console.log('after', action, controller.getState());
  };

  cleanup() {}
}
class TickerStream implements Manager {
  middleware: Middleware = controller => {
    this.handleMsg = msg => {
      controller.set(Ticker, { id: msg.id }, msg);
    };
    return next => action => next(action);
  };

  init() {
    this.websocket = new WebSocket('wss://ws-feed.myexchange.com');
    this.websocket.onmessage = event => {
      const msg = JSON.parse(event.data);
      this.handleMsg(msg);
    };
  }
  cleanup() {
    this.websocket.close();
  }
}

Integrated data mocking

const fixtures = [
  {
    endpoint: ArticleResource.getList,
    args: [{ maxResults: 10 }] as const,
    response: [
      {
        id: '5',
        title: 'first post',
        body: 'have a merry christmas',
        author: { id: '10', username: 'bob' },
        createdAt: new Date(0).toISOString(),
      },
      {
        id: '532',
        title: 'second post',
        body: 'never again',
        author: { id: '10', username: 'bob' },
        createdAt: new Date(0).toISOString(),
      },
    ],
  },
  {
    endpoint: ArticleResource.update,
    response: ({ id }, body) => ({
      ...body,
      id,
    }),
  },
];

const Story = () => (
  <MockResolver fixtures={options[result]}>
    <ArticleList maxResults={10} />
  </MockResolver>
);

...all typed ...fast ...and consistent

For the small price of 9kb gzipped. ย ย  ๐ŸGet started now

Features

Examples

  • Todo: GitHub | Sandbox | Edit on CodeSandbox
  • Github: GitHub | Sandbox
  • NextJS: GitHub | Sandbox | Edit on CodeSandbox
  • Websockets: GitHub | Sandbox | Website

API

Reactive Applications

Define Data

Networking definition

Data model
Data TypeMutableSchemaDescriptionQueryable
Objectโœ…Entity, EntityMixinsingle unique objectโœ…
โœ…Union(Entity)polymorphic objects (A | B)โœ…
๐Ÿ›‘Objectstatically known keys๐Ÿ›‘
Invalidate(Entity)delete an entity๐Ÿ›‘
Listโœ…Collection(Array)growable listsโœ…
๐Ÿ›‘Arrayimmutable lists๐Ÿ›‘
Alllist of all entities of a kindโœ…
Mapโœ…Collection(Values)growable mapsโœ…
๐Ÿ›‘Valuesimmutable maps๐Ÿ›‘
Scalarโœ…Scalarlens-dependent entity fieldsโœ…
anyQuery(Queryable)memoized custom transformsโœ…
Lazy(Schema)deferred denormalizationโœ…
expo
fetch
hooks
normalized
normalizr
react
reactive-programming
react-native
rest
sse-client
state-management
suspense
typescript

reactive/data-client

Async State Management without the Management. REST, GraphQL, SSE, Websockets

TypeScript

2,038

5,444 commits

updated Oct 12, 2026

See the code

README

Reactive Data Client

The scalable way to build applications with dynamic data.

Declarative resource definitions for REST, GraphQL, Websockets+SSE and more
Performant rendering in React, NextJS, React Native, Expo, Vue

Schema driven. Zero updater functions.

CircleCI Coverage Status Percentage of issues still open bundle size npm version PRs Welcome Agent Skills Chat

๐Ÿ“–Read The Docs ย |ย  ๐ŸGetting Started ย |ย  ๐Ÿค–Agent Skills
๐ŸŽฎ Demos: ย  Todo ย |ย  Github Social ย |ย  NextJS SSR ย |ย  Websockets+SSR

Installation

npm install --save @data-client/react @data-client/rest @data-client/test

For more details, see the Getting Started docs page.

Skills

npx skills add reactive/data-client

Then run skill "data-client-setup"

Usage

Simple TypeScript definition

class User extends Entity {
  id = '';
  username = '';
}

class Article extends Entity {
  id = '';
  title = '';
  body = '';
  author = User.fromJS();
  createdAt = Temporal.Instant.fromEpochMilliseconds(0);

  static schema = {
    author: User,
    createdAt: Temporal.Instant.from,
  };
}

Create collection of API Endpoints

const UserResource = resource({
  path: '/users/:id',
  schema: User,
  optimistic: true,
});

const ArticleResource = resource({
  path: '/articles/:id',
  schema: Article,
  searchParams: {} as { author?: string },
  optimistic: true,
  paginationField: 'cursor',
});

One line data binding

const article = useSuspense(ArticleResource.get, { id });
return (
  <article>
    <h2>
      {article.title} by {article.author.username}
    </h2>
    <p>{article.body}</p>
  </article>
);

Reactive Mutations

const ctrl = useController();
return (
  <>
    <CreateArticleForm
      onSubmit={article =>
        ctrl.fetch(ArticleResource.getList.push, article)
      }
    />
    <ProfileForm
      onSubmit={user =>
        ctrl.fetch(UserResource.update, { id: article.author.id }, user)
      }
    />
    <button onClick={() => ctrl.fetch(ArticleResource.delete, { id })}>
      Delete
    </button>
  </>
);

Subscriptions

const price = useLive(PriceResource.get, { symbol });
return price.value;

Type-safe Imperative Actions

const ctrl = useController();
await ctrl.fetch(ArticleResource.update, { id }, articleData);
await ctrl.fetchIfStale(ArticleResource.get, { id });
ctrl.expireAll(ArticleResource.getList);
ctrl.invalidate(ArticleResource.get, { id });
ctrl.invalidateAll(ArticleResource.getList);
ctrl.setResponse(ArticleResource.get, { id }, articleData);
ctrl.set(Article, { id }, articleData);

Programmatic queries

const queryTotalVotes = new Query(
  new Collection([BlogPost]),
  posts => posts.reduce((total, post) => total + post.votes, 0),
);

const totalVotes = useQuery(queryTotalVotes);
const totalVotesForUser = useQuery(queryTotalVotes, { userId });
const groupTodoByUser = new Query(
  TodoResource.getList.schema,
  todos => Object.groupBy(todos, todo => todo.userId),
);
const todosByUser = useQuery(groupTodoByUser);

Powerful Middlewares

class LoggingManager implements Manager {
  middleware: Middleware = controller => next => async action => {
    console.log('before', action, controller.getState());
    await next(action);
    console.log('after', action, controller.getState());
  };

  cleanup() {}
}
class TickerStream implements Manager {
  middleware: Middleware = controller => {
    this.handleMsg = msg => {
      controller.set(Ticker, { id: msg.id }, msg);
    };
    return next => action => next(action);
  };

  init() {
    this.websocket = new WebSocket('wss://ws-feed.myexchange.com');
    this.websocket.onmessage = event => {
      const msg = JSON.parse(event.data);
      this.handleMsg(msg);
    };
  }
  cleanup() {
    this.websocket.close();
  }
}

Integrated data mocking

const fixtures = [
  {
    endpoint: ArticleResource.getList,
    args: [{ maxResults: 10 }] as const,
    response: [
      {
        id: '5',
        title: 'first post',
        body: 'have a merry christmas',
        author: { id: '10', username: 'bob' },
        createdAt: new Date(0).toISOString(),
      },
      {
        id: '532',
        title: 'second post',
        body: 'never again',
        author: { id: '10', username: 'bob' },
        createdAt: new Date(0).toISOString(),
      },
    ],
  },
  {
    endpoint: ArticleResource.update,
    response: ({ id }, body) => ({
      ...body,
      id,
    }),
  },
];

const Story = () => (
  <MockResolver fixtures={options[result]}>
    <ArticleList maxResults={10} />
  </MockResolver>
);

...all typed ...fast ...and consistent

For the small price of 9kb gzipped. ย ย  ๐ŸGet started now

Features

Examples

  • Todo: GitHub | Sandbox | Edit on CodeSandbox
  • Github: GitHub | Sandbox
  • NextJS: GitHub | Sandbox | Edit on CodeSandbox
  • Websockets: GitHub | Sandbox | Website

API

Reactive Applications

Define Data

Networking definition

Data model
Data TypeMutableSchemaDescriptionQueryable
Objectโœ…Entity, EntityMixinsingle unique objectโœ…
โœ…Union(Entity)polymorphic objects (A | B)โœ…
๐Ÿ›‘Objectstatically known keys๐Ÿ›‘
Invalidate(Entity)delete an entity๐Ÿ›‘
Listโœ…Collection(Array)growable listsโœ…
๐Ÿ›‘Arrayimmutable lists๐Ÿ›‘
Alllist of all entities of a kindโœ…
Mapโœ…Collection(Values)growable mapsโœ…
๐Ÿ›‘Valuesimmutable maps๐Ÿ›‘
Scalarโœ…Scalarlens-dependent entity fieldsโœ…
anyQuery(Queryable)memoized custom transformsโœ…
Lazy(Schema)deferred denormalizationโœ…
expo
fetch
hooks
normalized
normalizr
react
reactive-programming
react-native
rest
sse-client
state-management
suspense
typescript

Significant stargazers

Luke Karrys

718 followers ยท starred Oct 2019

Guillermo Rauch

17,405 followers ยท starred Jul 2019

Tim Neutkens

6,144 followers ยท starred Aug 2019

Oscar Beaumont

414 followers ยท starred Oct 2022