{"id":20989078,"url":"https://github.com/aribouius/jsonapi-react","last_synced_at":"2025-04-05T20:08:31.958Z","repository":{"id":42511973,"uuid":"235689095","full_name":"aribouius/jsonapi-react","owner":"aribouius","description":"A minimal JSON:API client and React hooks for fetching, updating, and caching remote data.","archived":false,"fork":false,"pushed_at":"2024-01-22T16:08:25.000Z","size":330,"stargazers_count":149,"open_issues_count":15,"forks_count":27,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-03-29T19:04:33.850Z","etag":null,"topics":["cache","hooks","jsonapi","react","rest","server-side-rendering","stale-while-revalidate"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aribouius.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-01-22T23:34:16.000Z","updated_at":"2025-03-28T18:47:37.000Z","dependencies_parsed_at":"2024-06-19T17:40:17.538Z","dependency_job_id":"d7cb2cb9-33cd-4763-b192-07ad68aa03f6","html_url":"https://github.com/aribouius/jsonapi-react","commit_stats":{"total_commits":94,"total_committers":9,"mean_commits":"10.444444444444445","dds":"0.37234042553191493","last_synced_commit":"269296564cbaf1d980d2406711179c8928366d89"},"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aribouius%2Fjsonapi-react","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aribouius%2Fjsonapi-react/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aribouius%2Fjsonapi-react/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aribouius%2Fjsonapi-react/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aribouius","download_url":"https://codeload.github.com/aribouius/jsonapi-react/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247393570,"owners_count":20931813,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cache","hooks","jsonapi","react","rest","server-side-rendering","stale-while-revalidate"],"created_at":"2024-11-19T06:23:39.858Z","updated_at":"2025-04-05T20:08:31.932Z","avatar_url":"https://github.com/aribouius.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# jsonapi-react\nA minimal [JSON:API](https://jsonapi.org/) client and [React](https://reactjs.org/) hooks for fetching, updating, and caching remote data.\n\n\u003ca href=\"https://bundlephobia.com/result?p=jsonapi-react@latest\" target=\"\\_parent\"\u003e\n  \u003cimg src=\"https://badgen.net/bundlephobia/minzip/jsonapi-react@latest\" /\u003e\n\u003c/a\u003e\n\u003ca href=\"https://travis-ci.com/aribouius/jsonapi-react\" target=\"\\_parent\"\u003e\n  \u003cimg src=\"https://api.travis-ci.org/aribouius/jsonapi-react.svg?branch=master\" /\u003e\n\u003c/a\u003e\n\n## Features\n- Declarative API queries and mutations\n- JSON:API schema serialization + normalization\n- Query caching + garbage collection\n- Automatic refetching (stale-while-revalidate)\n- SSR support\n\n## Purpose\nIn short, to provide a similar client experience to using `React` + [GraphQL](https://graphql.org/).  \n\nThe `JSON:API` specification offers numerous benefits for writing and consuming REST API's, but at the expense of clients being required to manage complex schema serializations. There are [several projects](https://jsonapi.org/implementations/) that provide good `JSON:API` implementations,\nbut none offer a seamless integration with `React` without incorporating additional libraries and/or model abstractions.\n\nLibraries like [react-query](https://github.com/tannerlinsley/react-query) and [SWR](https://github.com/zeit/swr) (both of which are fantastic, and obvious inspirations for this project) go a far way in bridging the gap when coupled with a serialization library like [json-api-normalizer](https://github.com/yury-dymov/json-api-normalizer). But both require a non-trivial amount of cache invalidation configuration, given resources can be returned from any number of endpoints.  \n\n\n## Support\n- React 16.8 or later\n- Browsers [`\u003e 1%, not dead`](https://browserl.ist/?q=%3E+1%25%2C+not+dead)\n- Consider polyfilling:\n  - [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)\n  - [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)\n\n## Documentation\n- [Installation](#installation)\n- [Getting Started](#getting-started)\n- [Queries](#queries)\n- [Mutations](#mutations)\n- [Deleting](#deleting-resources)\n- [Caching](#caching)\n- [Manual Requests](#manual-requests)\n- [Server-Side Rendering](#server-side-rendering)\n- [API Reference](#api)\n  - [useQuery](#useQuery)\n  - [useMutation](#useMutation)\n  - [useIsFetching](#useClient)\n  - [useClient](#useClient)\n  - [ApiClient](#ApiClient)\n  - [ApiProvider](#ApiProvider)\n  - [renderWithData](#renderWithData)\n## Installation\n```\nnpm i --save jsonapi-react\n```\n\n## Getting Started\nTo begin you'll need to create an [ApiClient](#ApiClient) instance and wrap your app with a provider.\n```javascript\nimport { ApiClient, ApiProvider } from 'jsonapi-react'\nimport schema from './schema'\n\nconst client = new ApiClient({\n  url: 'https://my-api.com',\n  schema,\n})\n\nconst Root = (\n  \u003cApiProvider client={client}\u003e\n    \u003cApp /\u003e\n  \u003c/ApiProvider\u003e\n)\n\nReactDOM.render(\n  Root,\n  document.getElementById('root')\n)\n```\n\n### Schema Definition\nIn order to accurately serialize mutations and track which resource types are associated with each request, the `ApiClient` class requires a schema object that describes your API's resources and their relationships.\n\n```javascript\nnew ApiClient({\n  schema: {\n    todos: {\n      type: 'todos',\n      relationships: {\n        user: {\n          type: 'users',\n        }\n      }\n    },\n    users: {\n      type: 'users',\n      relationships: {\n        todos: {\n          type: 'todos',\n        }\n      }\n    }\n  }\n})\n```\n\nYou can also describe and customize how fields get deserialized.  Field configuration is entirely _additive_, so any omitted fields are simply passed through unchanged.\n```javascript\nconst schema = {\n  todos: {\n    type: 'todos',\n    fields: {\n      title: 'string', // shorthand\n      status: {\n        resolve: status =\u003e {\n          return status.toUpperCase()\n        },\n      },\n      created: {\n        type: 'date', // converts value to a Date object\n        readOnly: true // removes field for mutations\n      }\n    },\n    relationships: {\n      user: {\n        type: 'users',\n      }\n    }\n  },\n}\n```\n\n## Queries\nTo make a query, call the [useQuery](#useQuery) hook with the `type` of resource you are fetching. The returned object will contain the query result, as well as information relating to the request.\n```javascript\nimport { useQuery } from 'jsonapi-react'\n\nfunction Todos() {\n  const { data, meta, error, isLoading, isFetching } = useQuery('todos')\n\n  return (\n    \u003cdiv\u003e\n      isLoading ? (\n        \u003cdiv\u003e...loading\u003c/div\u003e\n      ) : (\n        data.map(todo =\u003e (\n          \u003cdiv key={todo.id}\u003e{todo.title}\u003c/div\u003e\n        ))\n      )\n    \u003c/div\u003e\n  )\n}\n```\n\nThe argument simply gets converted to an API endpoint string, so the above is equivalent to doing\n```javascript\nuseQuery('/todos')\n```\n\nAs syntactic sugar, you can also pass an array of URL segments.\n```javascript\nuseQuery(['todos', 1])\nuseQuery(['todos', 1, 'comments'])\n```\n\nTo apply refinements such as filtering, pagination, or included resources, pass an object of URL query parameters as the _last_ value of the array. The object gets serialized to a `JSON:API` compatible query string using [qs](https://github.com/ljharb/qs).\n```javascript\nuseQuery(['todos', {\n  filter: {\n    complete: 0,\n  },\n  include: [\n    'comments',\n  ],\n  page: {\n    number: 1,\n    size: 20,\n  },  \n}])\n```\n\nIf a query isn't ready to be requested yet, pass a _falsey_ value to defer execution.\n```javascript\nconst id = null\nconst { data: todos } = useQuery(id \u0026\u0026 ['users', id, 'todos'])\n```\n\n### Normalization\nThe API response data gets automatically deserialized into a nested resource structure, meaning this...\n```javascript\n{\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"todos\",\n    \"attributes\": {\n      \"title\": \"Clean the kitchen!\"\n    },\n    \"relationships\": {\n      \"user\": {\n        \"data\": {\n          \"type\": \"users\",\n          \"id\": \"2\"\n        }\n      },\n    },\n  },\n  \"included\": [\n    {\n      \"id\": 2,\n      \"type\": \"users\",\n      \"attributes\": {\n        \"name\": \"Steve\"\n      }\n    }\n  ],\n}\n```\n\nGets normalized to...\n```javascript\n{\n  id: \"1\",\n  title: \"Clean the kitchen!\",\n  user: {\n    id: \"2\",\n    name: \"Steve\"\n  }\n}\n```\n\n## Mutations\nTo run a mutation, first call the [useMutation](#useMutation) hook with a query key. The return value is a tuple that includes a `mutate` function, and an object with information related to the request. Then call the `mutate` function to execute the mutation, passing it the data to be submitted.\n```javascript\nimport { useMutation } from 'jsonapi-react'\n\nfunction AddTodo() {\n  const [title, setTitle] = useState('')\n  const [addTodo, { isLoading, data, error, errors }] = useMutation('todos')\n\n  const handleSubmit = async e =\u003e {\n    e.preventDefault()\n    const result = await addTodo({ title })\n  }\n\n  return (\n    \u003cform onSubmit={handleSubmit}\u003e\n      \u003cinput\n        type=\"text\"\n        value={title}\n        onChange={e =\u003e setTitle(e.target.value)}\n      /\u003e\n      \u003cbutton type=\"submit\"\u003eCreate Todo\u003c/button\u003e\n    \u003c/form\u003e\n  )\n}\n```\n\n### Serialization\nThe mutation function expects a [normalized](#normalization) resource object, and automatically handles serializing it. For example, this...\n```javascript\n{\n  id: \"1\",\n  title: \"Clean the kitchen!\",\n  user: {\n    id: \"1\",\n    name: \"Steve\",\n  }\n}\n```\n\nGets serialized to...\n```javascript\n{\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"todos\",\n    \"attributes\": {\n      \"title\": \"Clean the kitchen!\"\n    },\n    \"relationships\": {\n      \"user\": {\n        \"data\": {\n          \"type\": \"users\",\n          \"id\": \"1\"\n        }\n      }\n    }\n  }\n}\n```\n\n## Deleting Resources\n`jsonapi-react` doesn't currently provide a hook for deleting resources, because there's typically not much local state management associated with the action. Instead, deleting resources is supported through a [manual request](#manual-requests) on the `client` instance.\n\n\n## Caching\n`jsonapi-react` implements a `stale-while-revalidate` in-memory caching strategy that ensures queries are deduped across the application and only executed when needed.  Caching is disabled by default, but can be configured both globally, and/or per query instance.\n\n### Configuration\nCaching behavior is determined by two configuration values:\n- `cacheTime` - The number of seconds the response should be cached from the time it is received.\n- `staleTime` - The number of seconds until the response becomes stale. If a cached query that has become stale is requested, the cached response is returned, and the query is refetched in the background.  The refetched response is delivered to any active query instances, and re-cached for future requests.\n\nTo assign default caching rules for the whole application, configure the client instance.\n```javascript\nconst client = new ApiClient({\n  cacheTime: 5 * 60,\n  staleTime: 60,\n})\n```\n\nTo override the global caching rules, pass a configuration object to `useQuery`.\n```javascript\nuseQuery('todos', {\n  cacheTime: 5 * 60,\n  staleTime: 60,\n})\n```\n\n### Invalidation\nWhen performing mutations, there's a good chance one or more cached queries should get invalidated, and potentially refetched immediately.\n\nSince the JSON:API schema allows us to determine which resources (including relationships) were updated, the following steps are automatically taken after successful mutations:\n\n- Any cached results that contain resources with a `type` that matches either the mutated resource, or its included relationships, are invalidated and refetched for active query instances.\n- If a query for the mutated resource is cached, and the query URL matches the mutation URL (i.e. the responses can be assumed analogous), the cache is updated with the mutation result and delivered to active instances.  If the URL's don't match (e.g. one used refinements), then the cache is invalidated and the query refetched for active instances.\n\nTo override which resource types get invalidated as part of a mutation, the `useMutation` hook accepts a `invalidate` option.\n```JavaScript\nconst [mutation] = useMutation(['todos', 1], {\n  invalidate: ['todos', 'comments']\n})\n```\n\nTo prevent any invalidation from taking place, pass false to the `invalidate` option.\n```JavaScript\nconst [mutation] = useMutation(['todos', 1], {\n  invalidate: false\n})\n```\n\n## Manual Requests\nManual API requests can be performed through the client instance, which can be obtained with the [useClient](#useClient) hook\n\n```javascript\nimport { useClient } from 'jsonapi-react'\n\nfunction Todos() {\n  const client = useClient()\n}\n```\n\nThe client instance is also included in the object returned from the `useQuery` and `useMutation` hooks.\n```javascript\nfunction Todos() {\n  const { client } = useQuery('todos')\n}\n\nfunction EditTodo() {\n  const [mutate, { client }] = useMutation('todos')\n}\n```\nThe client request methods have a similar signature as the hooks, and return the same response structure.\n\n```javascript\n# Queries\nconst { data, error } = await client.fetch(['todos', 1])\n\n# Mutations\nconst { data, error, errors } = await client.mutate(['todos', 1], { title: 'New Title' })\n\n# Deletions\nconst { error } = await client.delete(['todos', 1])\n```\n\n## Server-Side Rendering\nFull SSR support is included out of the box, and requires a small amount of extra configuration on the server.\n\n```javascript\nimport { ApiProvider, ApiClient, renderWithData } from 'jsonapi-react'\n\nconst app = new Express()\n\napp.use(async (req, res) =\u003e {\n  const client = new ApiClient({\n    ssrMode: true,\n    url: 'https://my-api.com',\n    schema,\n  })\n\n  const Root = (\n    \u003cApiProvider client={client}\u003e\n      \u003cApp /\u003e\n    \u003c/ApiProvider\u003e\n  )\n\n  const [content, initialState] = await renderWithData(Root, client)\n\n  const html = \u003cHtml content={content} state={initialState} /\u003e\n\n  res.status(200)\n  res.send(`\u003c!doctype html\u003e\\n${ReactDOM.renderToStaticMarkup(html)}`)\n  res.end()\n})\n```\n\nThe above example assumes that the `Html` component exposes the `initialState` for client rehydration.\n```html\n\u003cscript\u003e\n  window.__APP_STATE__ = JSON.stringify(state)\n\u003c/script\u003e\n```\n\nOn the client side you'll then need to hydrate the client instance.\n```javascript\nconst client = new ApiClient({\n  url: 'https://my-api.com',,\n})\n\nclient.hydrate(\n  window.__APP_STATE__\n)\n```\n\nTo prevent specific queries from being fetched during SSR, the `useQuery` hook accepts a `ssr` option.\n```javascript\nconst result = useQuery('todos', { ssr: false })\n```\n\n## API\n\n### `useQuery`\n### Options\n- `queryArg: String | [String, Int, Params: Object] | falsey`\n  - A string, or array of strings/integers.\n  - Array may contain a query parameter object as the last value.\n  - If _falsey_, the query will not be executed.\n- `config: Object`\n  - `cacheTime: Int | null`:\n    - The number of seconds to cache the query.\n    - Defaults to client configuration value.\n  - `staleTime: Int | null`\n    - The number of seconds until the query becomes stale.\n    - Defaults to client configuration value.\n  - `ssr: Boolean`\n    - Set to `false` to disable server-side rendering of query.\n    - Defaults to context value.\n  - `client: ApiClient`\n    - An optional separate client instance.\n    - Defaults to context provided instance.\n### Result\n- `data: Object | Array | undefined`\n  - The normalized (deserialized) result from a successful request.\n- `meta: Object | undefined`\n  - A `meta` object returned from a successful request, if present.\n- `links: Object | undefined`\n  - A `links` object returned from a successful request, if present.\n- `error: Object | undefined`\n  - A request error, if thrown or returned from the server.\n- `refetch: Function`\n  - A function to manually refetch the query.\n- `setData: Function(Object | Array)`\n  - A function to manually update the local state of the `data` value.\n- `client: ApiClient`\n  - The client instance being used by the hook.\n\n### `useMutation`\n### Options\n- `queryArg: String | [String, Int, Params: Object]`\n  - A string, or array of strings/integers.\n  - Array may contain a query parameter object as the last value.\n- `config: Object`\n  - `invalidate: String | Array`\n    - One or more resource types to whose cache entries should be invalidated.\n  - `method: String`\n    - The request method to use.  Defaults to `POST` when creating a resource, and `PATCH` when updating.\n  - `client: ApiClient`\n    - An optional separate client instance.\n    - Defaults to context provided instance.\n### Result\n- `mutate: Function(Object | Array)`\n  - The mutation function you call with resource data to execute the mutation.\n  - Returns a promise that resolves to the result of the mutation.\n- `data: Object | Array | undefined`\n  - The normalized (deserialized) result from a successful request.\n- `meta: Object | undefined`\n  - A `meta` object returned from a successful request, if present.\n- `links: Object | undefined`\n  - A `links` object returned from a successful request, if present.\n- `error: Object | undefined`\n  - A request error, if thrown or returned from the server.\n- `errors: Array | undefined`\n  - Validation errors returned from the server.\n- `isLoading: Boolean`\n  - Indicates whether the mutation is currently being submitted.\n- `client: ApiClient`\n  - The client instance being used by the hook.\n\n### `useIsFetching`\n### Result\n- `isFetching: Boolean`\n  - Returns `true` if any query in the application is fetching.\n\n### `useClient`\n### Result\n- `client: ApiClient`\n  - The client instance on the current context.\n\n### `ApiClient`\n- `url: String`\n  - The full URL of the remote API.\n- `mediaType: String`\n  - The media type to use in request headers.\n  - Defaults to `application/vnd.api+json`.\n- `cacheTime: Int | Null`:\n  - The number of seconds to cache the query.\n  - Defaults to `0`.\n- `staleTime: Int | null`\n  - The number of seconds until the query becomes stale.\n  - Defaults to `null`.\n- `headers: Object`\n  - Default headers to include on every request.\n- `ssrMode: Boolean`\n  - Set to `true` when running in a server environment.\n  - Defaults to result of `typeof window === 'undefined'`.\n- `formatError: Function(error)`\n  - A function that formats a response error object.\n- `formatErrors: Function(errors)`\n  - A function that formats a validation error objects.\n- `fetch: Function(url, options)`\n  - Fetch implementation - defaults to the global `fetch`.\n- `fetchOptions: Object`\n  - Default options to use when calling `fetch`.\n  - See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) for available options.\n### Methods\n- `fetch(queryKey: String | [String, Int, Params: Object], [config]: Object)`\n  - Submits a query request.\n- `mutate(queryKey: String | [String, Int, Params: Object], data: Object | Array, [config]: Object)`\n  - Submits a mutation request.\n- `delete(queryKey: String | [String, Int, Params: Object], [config]: Object)`\n  - Submits a delete request.\n- `clearCache()`\n  - Clears all cached requests.\n- `addHeader(key: String, value: String)`\n  - Adds a default header to all requests.\n- `removeHeader(key: String)`\n  - Removes a default header.\n- `isFetching()`\n  - Returns `true` if a query is being fetched by the client.\n- `subscribe(callback: Function)`\n  - Subscribes an event listener to client requests.\n  - Returns a unsubscribe function.\n- `hydrate(state: Array)`\n  - Hydrates a client instance with state after SSR.\n\n### `ApiProvider`\n### Options\n- `client: ApiClient`\n  - The API client instance that should be used by the application.\n\n### `renderWithData`\n### Options\n- `element: Object`\n  - The root React element of the application.\n- `client: ApiClient`\n  - The client instance used during rendering.\n### Result\n- `content: String`\n  - The rendered application string.\n- `initialState: Array`\n  - The extracted client state.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faribouius%2Fjsonapi-react","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faribouius%2Fjsonapi-react","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faribouius%2Fjsonapi-react/lists"}