{"id":26579005,"url":"https://github.com/m10c/api-read-hook","last_synced_at":"2026-04-15T18:31:12.364Z","repository":{"id":42899453,"uuid":"252557811","full_name":"m10c/api-read-hook","owner":"m10c","description":"Hook-based library for simple yet flexible data fetching and display in React apps","archived":false,"fork":false,"pushed_at":"2023-08-02T15:44:27.000Z","size":2795,"stargazers_count":1,"open_issues_count":19,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-08-09T01:34:24.116Z","etag":null,"topics":["api","data-fetching","hooks","react","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/m10c.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","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-04-02T20:23:16.000Z","updated_at":"2021-12-18T15:31:50.000Z","dependencies_parsed_at":"2025-03-23T05:21:04.899Z","dependency_job_id":"435cbd4d-78b6-4e48-a763-fa24a62c9672","html_url":"https://github.com/m10c/api-read-hook","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/m10c/api-read-hook","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/m10c%2Fapi-read-hook","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/m10c%2Fapi-read-hook/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/m10c%2Fapi-read-hook/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/m10c%2Fapi-read-hook/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/m10c","download_url":"https://codeload.github.com/m10c/api-read-hook/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/m10c%2Fapi-read-hook/sbom","scorecard":{"id":608243,"data":{"date":"2025-08-11","repo":{"name":"github.com/m10c/api-read-hook","commit":"0fe05002cdb72976ba4fc7e3b02e34ef899c248e"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":1.7,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":9,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Warn: project license file does not contain an FSF or OSI license."],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":0,"reason":"62 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-67hx-6x53-jw92","Warn: Project is vulnerable to: GHSA-h5c3-5r3r-rr8q","Warn: Project is vulnerable to: GHSA-rmvr-2pp2-xj38","Warn: Project is vulnerable to: GHSA-xx4v-prfh-6cgc","Warn: Project is vulnerable to: GHSA-v88g-cgmw-v5xw","Warn: Project is vulnerable to: GHSA-93q8-gq69-wqmw","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-w8qv-6jwh-64r5","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-gxpj-cx7g-858c","Warn: Project is vulnerable to: GHSA-w573-4hg7-7wgq","Warn: Project is vulnerable to: GHSA-ff7x-qrg7-qggm","Warn: Project is vulnerable to: GHSA-phwq-j96m-2c2q","Warn: Project is vulnerable to: GHSA-ghr5-ch3p-vcr6","Warn: Project is vulnerable to: GHSA-2j2x-2gpw-g8fm","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-ww39-953v-wcq6","Warn: Project is vulnerable to: GHSA-pfrx-2q88-qq97","Warn: Project is vulnerable to: GHSA-765h-qjxv-5f44","Warn: Project is vulnerable to: GHSA-f2jv-r9rf-7988","Warn: Project is vulnerable to: GHSA-43f8-2h32-f4cj","Warn: Project is vulnerable to: GHSA-rc47-6667-2j5j","Warn: Project is vulnerable to: GHSA-qqgx-2p2h-9c37","Warn: Project is vulnerable to: GHSA-896r-f27r-55mw","Warn: Project is vulnerable to: GHSA-9c47-m6qq-7p4h","Warn: Project is vulnerable to: GHSA-p6mc-m468-83gw","Warn: Project is vulnerable to: GHSA-29mw-wpgm-hmr9","Warn: Project is vulnerable to: GHSA-35jh-r3h4-6jhm","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3","Warn: Project is vulnerable to: GHSA-xvch-5gv4-984h","Warn: Project is vulnerable to: GHSA-w7rc-rwvf-8q5r","Warn: Project is vulnerable to: GHSA-r683-j2x4-v87g","Warn: Project is vulnerable to: GHSA-5fw9-fq32-wv5p","Warn: Project is vulnerable to: GHSA-px4h-xg32-q955","Warn: Project is vulnerable to: GHSA-3j8f-xvm3-ffx4","Warn: Project is vulnerable to: GHSA-4p35-cfcx-8653","Warn: Project is vulnerable to: GHSA-7f3x-x4pr-wqhj","Warn: Project is vulnerable to: GHSA-jpp7-7chh-cf67","Warn: Project is vulnerable to: GHSA-q6wq-5p59-983w","Warn: Project is vulnerable to: GHSA-j9fq-vwqv-2fm2","Warn: Project is vulnerable to: GHSA-pqw5-jmp5-px4v","Warn: Project is vulnerable to: GHSA-hj48-42vr-x3v9","Warn: Project is vulnerable to: GHSA-hrpp-h998-j3pp","Warn: Project is vulnerable to: GHSA-p8p7-x288-28g6","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw","Warn: Project is vulnerable to: GHSA-44c6-4v22-4mhx","Warn: Project is vulnerable to: GHSA-4x5v-gmq8-25ch","Warn: Project is vulnerable to: GHSA-4rq4-32rv-6wp6","Warn: Project is vulnerable to: GHSA-64g7-mvw6-v9qj","Warn: Project is vulnerable to: GHSA-52f5-9888-hmc6","Warn: Project is vulnerable to: GHSA-jgrx-mgxx-jf9v","Warn: Project is vulnerable to: GHSA-72xf-g2v4-qvf3","Warn: Project is vulnerable to: GHSA-7p7h-4mm5-852v","Warn: Project is vulnerable to: GHSA-38fc-wpqx-33j7","Warn: Project is vulnerable to: GHSA-j8xg-fqg3-53r7","Warn: Project is vulnerable to: GHSA-6fc8-4gx4-v693","Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q","Warn: Project is vulnerable to: GHSA-c4w7-xm78-47vh","Warn: Project is vulnerable to: GHSA-p9pc-299p-vxgp"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-21T02:05:28.900Z","repository_id":42899453,"created_at":"2025-08-21T02:05:28.900Z","updated_at":"2025-08-21T02:05:28.900Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31854608,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-15T15:24:51.572Z","status":"ssl_error","status_checked_at":"2026-04-15T15:24:39.138Z","response_time":63,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["api","data-fetching","hooks","react","typescript"],"created_at":"2025-03-23T05:20:06.852Z","updated_at":"2026-04-15T18:31:12.330Z","avatar_url":"https://github.com/m10c.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# api-read-hook\n\n[![npm version](https://img.shields.io/npm/v/api-read-hook.svg)](https://www.npmjs.com/package/api-read-hook)\n\nHook-based library for simple yet flexible data fetching and display in React apps.\n\n- Compatible with any data fetching backend\n  (e.g. [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API))\n  and API format (e.g. REST, GraphQL).\n- Predictable default mode of operation:\n  fresh data is always fetched when the component first mounts.\n- Pagination support (flexible enough for any server mechanism).\n- Manual invalidation controls to wipe out stale responses across all components, including automatic time-based invalidation.\n- Manual mutation controls to update responses across mounted components.\n- Configurable per-instance or via React context.\n- Full type-safety with TypeScript or Flow,\n  including easy typing of expected responses.\n- Supports chaining API multiple dependent API requests.\n- Opt-in fine-grained control of a response cache,\n  with clear identification of stale responses and their age.\n- **TODO**: A global cache,\n  which can persist across screen unmounts.\n\n## Installation\n\n```bash\n$ npm install --save api-read-hook\n\n# Or, with Yarn\n$ yarn add api-read-hook\n```\n\n## Getting started\n\n```tsx\nimport { ApiReadProvider, useApiRead } from 'api-read-hook';\n\n/**\n * Build a specific reader using HTTP Fetch on a bespoke REST API\n */\nasync function myApiReader(path) {\n  const response = await fetch(`https://example.com${path}`);\n  if (!response.ok) throw new Error(`${path} returned ${response.status}`);\n  const data = await response.json();\n  return data;\n}\n\nfunction MyApp() {\n  return (\n    {/* Context configuration for all useApiRead calls */}\n    \u003cApiReadProvider config={{ reader: myApiReader }}\u003e\n      \u003cNavigation /\u003e\n    \u003c/ApiReadProvider\u003e\n  );\n}\n\nfunction HomeScreen() {\n  const { data, error } = useApiRead('/posts');\n\n  if (error) return 'An error occurred!';\n  if (!data) return 'Loading...';\n\n  return (\n    \u003cul\u003e\n      {data.map(post =\u003e (\n        \u003cPost key={post.id} post={post} /\u003e\n      ))}\n    \u003c/ul\u003e\n  );\n}\n```\n\n## Rationale\n\n### Compared with SWR\n\nSWR is designed around (and even named after)\nthe model of stale data being shown to the user by defualt,\nwhile new data fetches in the background.\n\nIn our view,\nthis isn't desirable behaviour in the vast majority of cases for dynamic apps\n(particularly when considering React Native).\n\napi-read-hook uses a simpler and more predictable model by default,\nwhere when a screen mounts,\nit always fetches fresh data,\nand the user sees a loading state.\n\nThis is in our opinion more intuitive for the user,\nas they can trust they're seeing the latest data every time they open a new screen,\nnot worry the app may be opaquely digging up outdated stale data.\n\n3 years after this library was created based on this justification,\nthe situation seems unchanged.\nVercel (SWR maintainers) are stil pushing ahead with their vision that data should be stale by default,\ndespite much pushback:\nhttps://github.com/vercel/next.js/issues/42991#issuecomment-1569986363\n\n## API reference\n\n### `reader` function\n\nThe `reader` function is the only required piece of setup to make this library functional.\nIt's how the library knows how to perform requests against your API\n(which could be REST, GraphQL, or any other type of protocol).\n\nYour `reader` function,\nwhen provided with a `string` path\n(essentially a unique \"key\" for the request),\nshould asynchronously perform the appropriate request against your API,\ncarry out any processing on the response (e.g. parsing JSON),\nand returns it.\n\nIf the request fails,\nyou should throw an `Error`.\nFeel free to extend `Error` adding as much extra metadata as you'd like to use in your app.\n\n### Configuration options\n\nThe library can be configured with the `ReadConfig` options,\neither as a second argument to `useApiRead` to change just a single instance,\nor in the top level `ApiReadProvider` `config` prop to set the defaults for all instances.\n\n- **`reader`**: `(path: string) =\u003e Promise\u003cmixed\u003e` -\n  A reader function for your app's API, as described above.\n- **`staleWhileInvalidated?`**: `boolean` -\n  If `true`, then when an API response is considered invalidated\n  (either because you manually invalidated it, or it's become too old),\n  the stale (invalidated) response will continue to be returned by the `useApiRead` hook.\n- **`staleWhileError?`**: `boolean` -\n  If `true`, then when an API response returns an error,\n  if there was a previous response returned by this hook,\n  that will continue to be returned by the hook.\n- **`invalidateAge?`**: `number` -\n  If provided, defines the number of seconds after which the current response will be marked as \"invalidated\",\n  and therefore re-fetched.\n  This is useful to ensure the user is never looking at content which is too old,\n  even if they've kept the same screen open for a long time.\n\n### Read result\n\nThe `useApiRead` hook returns a `ReadResult` object.\nThe following properties tell you about the data returned:\n\n- **`data`**: `T | undefined` -\n  If the data hasn't been fetched yet,\n  or has been invalidated without the `staleWhileInvalidated` option enabled,\n  this will return `undefined`.\n  You will want to check for the absence of `data` and use that to display a loading screen.\n  Once the request succeeds,\n  it will return the data from your `reader` function.\n  In TypeScript/Flow, you can use generics\n  (`useApiRead\u003c{ expected: string }\u003e('/path')`)\n  to indicate the type `T` of this property.\n- **`error`**: `Error | undefined` -\n  If the request failed,\n  this can be used to display a relevant error message to the user.\n- **`stale`**: `boolean` -\n  Will be set to `true` if the current `data` being returned is considered stale.\n- **`staleReason`**: `null | 'invalidated' | 'error'` -\n  If a stale response is being returned (`stale: true`),\n  indicates why.\n- **`receivedAt`**: `null | number` -\n  If `data` is being returned,\n  a unix timestamp (seconds) of when the data was received\n  (i.e. when the `reader` returned the data).\n\nThe `ReadResult` also contains some functions which can be called to manipulate the current response:\n\n- **`invalidate`**: `() =\u003e void` -\n  Call this when you know the current response is invalid and needs refetching\n  (e.g. because the user just saved a change to the entity,\n  and you want to re-fetch the authoritative state).\n- **`mutate`**: `(data: T) =\u003e T` -\n  Make a direct change to the response payload\n  (e.g. because the user just saved a change to the entity,\n  which was simple enough that you can apply it directly).\n\nFinally, `ReadResult` also contains some properties that can be used for pagination:\n\n- **`readMore`**: `(path: string, updater: (moreData: T) =\u003e T) =\u003e void`\n- **`loadingMore`**: `boolean`\n- **`moreError`**: `Error` | `undefined`\n\n### `useInvalidation` hook\n\nWithin any component,\nthe `useInvalidation` hook can be used to retrieve helper functions for invalidating responses in other components:\n\n- **`invalidateExact`**: `(search: string) =\u003e void` -\n  Searches through all currently held responses\n  (i.e. any mounted hooks current displaying a response),\n  and invalidates those where `path` is an **exact** match of `search`.\n- **`invalidateMatching`**: `(search: string | RegExp) =\u003e void` -\n  Searchs through all currently held responses,\n  and invalidates those where:\n  - `search` is a `string`: `path` **contains** `search`.\n  - `search` is a `RegExp`: `path` matches the `search` regexp.\n### `useMutation` hook\n\nWorks in a similar way to `useInvalidation`, but for mutation:\n\n- **`mutateExact`**: `(search: string, mutator: \u003cT\u003e(data: T) =\u003e T) =\u003e void`\n- **`mutateMatching`**: `(search: string | RegExp, mutator: \u003cT\u003e(data: T) =\u003e T) =\u003e void`\n\n## Examples\n\n### Path of one API call dependent on another\n\nApart from splitting the hooks across 2 components,\nand having the parent only render the child once its API call has succeeded,\nyou can also approach this by passing `null` to `path` for the 2nd hook\n(which bypasses your `reader`)\nuntil you have enough information to construct the 2nd path:\n\n```js\nfunction ProfileScreen({ name }: Props) {\n  const apiSearch = useApiRead(`/find-user?name=${name}`);\n  const apiUser = useApiRead(\n    apiSearch.data ? `/profiles/${apiSearch.data.id}` : null\n  );\n\n  if (apiSearch.error || apiUser.error) return 'An error occurred!';\n  if (!apiUser.data) return 'Loading...';\n  const user = apiUser.data;\n\n  return \u003ch1\u003e{user.name}\u003c/h1\u003e;\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fm10c%2Fapi-read-hook","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fm10c%2Fapi-read-hook","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fm10c%2Fapi-read-hook/lists"}