{"id":47777127,"url":"https://github.com/afiiif/floppy-disk","last_synced_at":"2026-04-15T17:01:09.494Z","repository":{"id":188599635,"uuid":"677863376","full_name":"afiiif/floppy-disk","owner":"afiiif","description":"Lightweight, simple, and powerful state management library. The alternative for both Zustand \u0026 TanStack Query!! 🤯","archived":false,"fork":false,"pushed_at":"2026-04-13T17:11:29.000Z","size":2490,"stargazers_count":90,"open_issues_count":0,"forks_count":3,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-13T18:27:17.871Z","etag":null,"topics":["async","global-state-management","hooks","query","react","stores"],"latest_commit_sha":null,"homepage":"https://afiiif.github.io/floppy-disk/","language":"TypeScript","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/afiiif.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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-08-12T22:30:55.000Z","updated_at":"2026-04-13T12:16:47.000Z","dependencies_parsed_at":"2026-04-06T15:01:30.568Z","dependency_job_id":null,"html_url":"https://github.com/afiiif/floppy-disk","commit_stats":null,"previous_names":["afiiif/floppy-disk"],"tags_count":123,"template":false,"template_full_name":null,"purl":"pkg:github/afiiif/floppy-disk","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/afiiif%2Ffloppy-disk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/afiiif%2Ffloppy-disk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/afiiif%2Ffloppy-disk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/afiiif%2Ffloppy-disk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/afiiif","download_url":"https://codeload.github.com/afiiif/floppy-disk/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/afiiif%2Ffloppy-disk/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31851057,"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":["async","global-state-management","hooks","query","react","stores"],"created_at":"2026-04-03T12:04:06.272Z","updated_at":"2026-04-15T17:01:09.445Z","avatar_url":"https://github.com/afiiif.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# FloppyDisk.ts 💾\n\nA unified state model for **sync \u0026 async** data.\n\nIf you know [Zustand](https://zustand.docs.pmnd.rs) \u0026 [TanStack-Query](https://tanstack.com/query), you already know FloppyDisk.\\\nIt keeps what works, removes unnecessary complexity, and unifies everything into a simpler API.\\\nNo relearning—just a better experience.\n\n_Smaller bundle. Zero dependencies._\n\nDemo: https://afiiif.github.io/floppy-disk/\n\n**Installation:**\n\n```\nnpm install floppy-disk\n```\n\n## Global Store\n\nHere's how to create and use a store:\n\n```tsx\nimport { createStore } from \"floppy-disk/react\";\n\nconst useDigimon = createStore({\n  age: 7,\n  level: \"Rookie\",\n});\n```\n\nYou can use the store both inside and outside of React components.\n\n```tsx\nfunction MyDigimon() {\n  const { age } = useDigimon();\n  return \u003cdiv\u003eDigimon age: {age}\u003c/div\u003e;\n  // This component will only re-render when `age` changes.\n  // Changes to `level` will NOT trigger a re-render.\n}\n\nfunction Control() {\n  return (\n    \u003c\u003e\n      \u003cbutton\n        onClick={() =\u003e {\n          // You can setState directly\n          useDigimon.setState((prev) =\u003e ({ age: prev.age + 1 }));\n        }}\n      \u003e\n        Increase digimon's age\n      \u003c/button\u003e\n\n      \u003cbutton onClick={evolve}\u003eEvolve\u003c/button\u003e\n    \u003c/\u003e\n  );\n}\n\n// You can create a custom actions\nconst evolve = () =\u003e {\n  const { level } = useDigimon.getState();\n\n  const order = [\"In-Training\", \"Rookie\", \"Champion\", \"Ultimate\"];\n  const nextLevel = order[order.indexOf(level) + 1];\n\n  if (!nextLevel) return console.warn(\"Already at ultimate level\");\n\n  useDigimon.setState({ level: nextLevel });\n};\n```\n\n### Store Subscription\n\nAt its core, FloppyDisk is a **pub-sub store**.\n\nYou can subscribe manually:\n\n```tsx\nconst unsubscribe = useMyStore.subscribe((state, prev) =\u003e {\n  console.log(\"New state:\", state);\n});\n\n// Later\nunsubscribe();\n```\n\nFloppyDisk provides lifecycle hooks tied to subscription count.\n\n```tsx\nconst useTowerDefense = createStore(\n  { archers: 3, mages: 1, barracks: 2, artillery: 1 },\n  {\n    onFirstSubscribe: () =\u003e {\n      console.log(\"First subscriber! We’re officially popular 🎉\");\n    },\n    onSubscribe: () =\u003e {\n      console.log(\"New subscriber joined. Welcome aboard 🫡\");\n    },\n    onUnsubscribe: () =\u003e {\n      console.log(\"Subscriber left... was it something I said? 😭\");\n    },\n    onLastUnsubscribe: () =\u003e {\n      console.log(\"Everyone left. Guess I’ll just exist quietly now...\");\n    },\n  },\n);\n```\n\n### Differences from Zustand\n\nIf you're coming from Zustand, this should feel very familiar.\\\nKey differences:\n\n1. **No Selectors Needed**\\\n   You don't need selectors when using hooks.\n   FloppyDisk automatically tracks which parts of the state are used and optimizes re-renders accordingly.\n2. **Object-Only Store Initialization**\\\n   In FloppyDisk, stores **must** be initialized with an object. Primitive values or function initializers are not allowed.\n\nZustand examples:\n\n```tsx\nconst useDate = create(new Date(2021, 01, 11));\n\nconst useCounter = create((set) =\u003e ({\n  value: 1,\n  increment: () =\u003e set((prev) =\u003e ({ value: prev.value + 1 })),\n}));\n```\n\nFloppyDisk equivalents:\n\n```tsx\nconst useDate = createStore({ value: new Date(2021, 01, 11) });\n\nconst useCounter = createStore({ value: 1 });\nconst increment = () =\u003e useCounter.setState((prev) =\u003e ({ value: prev.value + 1 }));\n// Unlike Zustand, defining actions inside the store is **discouraged** in FloppyDisk.\n// This improves tree-shakeability and keeps your store minimal.\n\n// However, it's still possible to mix actions with the state if you understand how closures work:\nconst useCounterAlt = createStore({\n  value: 1,\n  increment: () =\u003e useCounterAlt.setState((prev) =\u003e ({ value: prev.value + 1 })),\n});\n```\n\n## Async State (Query \u0026 Mutation)\n\nFloppyDisk also provides a powerful async state layer, inspired by [TanStack-Query](https://tanstack.com/query) but with a simpler API.\n\nIt is agnostic to the type of async operation,\nit works with any Promise-based operation—whether it's a network request, local computation, storage access, or something else.\n\nBecause of that, we intentionally avoid terms like \"fetch\" or \"refetch\".\\\nInstead, we use:\n\n- **execute** → run the async operation (same as \"fetch\" in TanStack-Query)\n- **revalidate** → re-run while keeping existing data (same as \"refetch\" in TanStack-Query)\n\n### Query vs Mutation\n\n\u003cdetails\u003e\n\n\u003csummary\u003eQuery → Read Operations\u003c/summary\u003e\n\nQueries are designed for reading data.\\\nThey assume:\n\n- no side effects\n- no data mutation\n- safe to run multiple times\n\nBecause of this, queries come with helpful defaults:\n\n- ✅ Retry mechanism (for transient failures)\n- ✅ Revalidation (keep data fresh automatically)\n- ✅ Caching \u0026 staleness control\n\nUse queries when:\n\n- fetching data\n- reading from storage\n- running idempotent async logic\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\n\u003csummary\u003eMutation → Write Operations\u003c/summary\u003e\n\nMutations are designed for changing data.\\\nExamples:\n\n- insert\n- update\n- delete\n- triggering side effects\n\nBecause mutations are **not safe to repeat blindly**, FloppyDisk does **not** include:\n\n- ❌ automatic retry\n- ❌ automatic revalidation\n- ❌ implicit re-execution\n\nThis is intentional.\\\nMutations should be explicit and controlled, not automatic.\n\nIf you need retry mechanism, then you can always add it manually.\n\n\u003c/details\u003e\n\n### Single Query\n\nCreate a query using `createQuery`:\n\n```tsx\nimport { createQuery } from \"floppy-disk/react\";\n\nconst myCoolQuery = createQuery(\n  myAsyncFn,\n  // { staleTime: 5000, revalidateOnFocus: false } \u003c-- optional options\n);\n\nconst useMyCoolQuery = myCoolQuery();\n\n// Use it inside your component:\n\nfunction MyComponent() {\n  const query = useMyCoolQuery();\n  if (query.state === \"INITIAL\") return \u003cdiv\u003eLoading...\u003c/div\u003e;\n  if (query.error) return \u003cdiv\u003eError: {query.error.message}\u003c/div\u003e;\n  return \u003cdiv\u003e{JSON.stringify(query.data)}\u003c/div\u003e;\n}\n```\n\n### Query State: Two Independent Dimensions\n\nFloppyDisk tracks two things separately:\n\n- Is it running? → `isPending`\\\n  (value: `boolean`)\n- What's the result? → `state`\\\n  (value: `INITIAL | 'SUCCESS' | 'ERROR' | 'SUCCESS_BUT_REVALIDATION_ERROR'`)\n\nThey are **independent**.\n\n### Automatic Re-render Optimization\n\nJust like the global store, FloppyDisk tracks usage automatically:\n\n```tsx\nconst { data } = useMyQuery();\n// ^Only data changes will trigger a re-render\n\nconst value = useMyQuery().data?.foo.bar.baz;\n// ^Only data.foo.bar.baz changes will trigger a re-render\n```\n\n### Keyed Query (Dynamic Params)\n\nYou can create parameterized queries:\n\n```tsx\nimport { getUserById, type GetUserByIdResponse } from \"../utils\";\n\ntype MyQueryParam = { id: string };\n\nconst userQuery = createQuery\u003cGetUserByIdResponse, MyQueryParam\u003e(\n  getUserById,\n  // { staleTime: 5000, revalidateOnFocus: false } \u003c-- optional options\n);\n```\n\nUse it with parameters:\n\n```tsx\nfunction UserDetail({ id }) {\n  const useUserQuery = userQuery({ id: 1 });\n  const query = useUserQuery();\n  if (query.state === \"INITIAL\") return \u003cdiv\u003eLoading...\u003c/div\u003e;\n  if (query.error) return \u003cdiv\u003eError: {query.error.message}\u003c/div\u003e;\n  return \u003cdiv\u003e{JSON.stringify(query.data)}\u003c/div\u003e;\n}\n```\n\nEach unique parameter creates its own cache entry.\n\n### Infinite Query\n\nFloppyDisk does **not provide** a dedicated \"infinite query\" API.\\\nInstead, it embraces a simpler and more flexible approach:\n\n\u003e Infinite queries are just **composition** + **recursion**.\n\nWhy? Because async state is already powerful enough:\n\n- keyed queries handle parameters\n- components handle composition\n- recursion handles pagination\n\nNo special abstraction needed.\n\nHere is the example on how to implement infinite query properly:\n\n```tsx\ntype GetPostParams = {\n  cursor?: string; // For pagination\n};\ntype GetPostsResponse = {\n  posts: Post[];\n  meta: { nextCursor: string };\n};\n\nconst postsQuery = createQuery\u003cGetPostsResponse, GetPostParams\u003e(getPosts, {\n  staleTime: Infinity,\n  revalidateOnFocus: false,\n  revalidateOnReconnect: false,\n});\n\nfunction Main() {\n  return \u003cPage cursor={undefined} /\u003e;\n}\n\nfunction Page({ cursor }: { cursor?: string }) {\n  const usePostsQuery = postsQuery({ cursor });\n  const { state, data, error } = usePostsQuery();\n\n  if (state === \"INITIAL\") return \u003cdiv\u003eLoading...\u003c/div\u003e;\n  if (error) return \u003cdiv\u003eError\u003c/div\u003e;\n\n  return (\n    \u003c\u003e\n      {data.posts.map((post) =\u003e (\n        \u003cPostCard key={post.id} post={post} /\u003e\n      ))}\n      {data.meta.nextCursor \u0026\u0026 \u003cLoadMore nextCursor={data.meta.nextCursor} /\u003e}\n    \u003c/\u003e\n  );\n}\n\nfunction LoadMore({ nextCursor }: { nextCursor?: string }) {\n  const [isNextPageRequested, setIsNextPageRequested] = useState(() =\u003e {\n    const stateOfNextPageQuery = postsQuery({ cursor: nextCursor }).getState();\n    return stateOfNextPageQuery.isPending || stateOfNextPageQuery.isSuccess;\n  });\n\n  if (isNextPageRequested) {\n    return \u003cPage cursor={nextCursor} /\u003e;\n  }\n\n  return \u003cBottomObserver onReachBottom={() =\u003e setIsNextPageRequested(true)} /\u003e;\n}\n```\n\nWhen implementing infinite queries, it is **highly recommended to disable automatic revalidation**.\n\nWhy?\\\nIn an infinite list, users may scroll through many pages (\"_doom-scrolling_\").\\\nIf revalidation is triggered:\n\n- All previously loaded pages may re-execute\n- Content at the top may change without the user noticing\n- Layout shifts can occur unexpectedly\n\nThis leads to a **confusing and unstable user experience**.\\\nRevalidating dozens of previously viewed pages rarely provides value to the user.\n\n## SSR Guidance\n\nExamples for using stores and queries in SSR with isolated data (no shared state between users).\n\n### Initialize Store State from Server\n\n```tsx\nconst useCountStore = createStore({ count: 0 });\n\nfunction Page({ initialCount }) {\n  const { count } = useCountStore({\n    initialState: { count: initialCount }, // e.g. 3\n  });\n\n  return \u003c\u003ecount is {count}\u003c/\u003e; // Output: count is 3\n}\n```\n\n### Initialize Query Data from Server\n\n```tsx\nasync function MyServerComponent() {\n  const data = await getData(); // e.g. { count: 3 }\n  return \u003cMyClientComponent initialData={data} /\u003e;\n}\n\nconst myQuery = createQuery(getData);\nconst useMyQuery = myQuery();\n\nfunction MyClientComponent({ initialData }) {\n  const { data } = useMyQuery({\n    initialData: initialData,\n    // initialDataIsStale: true \u003c-- Optional, default to false (no immediate revalidation)\n  });\n\n  return \u003c\u003ecount is {data.count}\u003c/\u003e; // Output: count is 3\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fafiiif%2Ffloppy-disk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fafiiif%2Ffloppy-disk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fafiiif%2Ffloppy-disk/lists"}