From 12fb94530a249ec3943b3081cb310dda7de99c6f Mon Sep 17 00:00:00 2001 From: Chuan-Heng Hsiao <2970164+chhsiao1981@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:08:40 -0400 Subject: [PATCH] update docs for 16.1.2 --- doc-src/00-introduction.md | 8 ++-- doc-src/03-how-it-works.md | 26 ++++++++++--- docs/00-introduction/index.html | 8 ++-- docs/03-how-it-works/index.html | 67 ++++++++++++++++++++++++--------- docs/search/search_index.json | 2 +- 5 files changed, 81 insertions(+), 30 deletions(-) diff --git a/doc-src/00-introduction.md b/doc-src/00-introduction.md index 3bcdd3d4..3a2942ec 100644 --- a/doc-src/00-introduction.md +++ b/doc-src/00-introduction.md @@ -21,9 +21,11 @@ However, there are several caveats of React Redux / RTK: Recently [zustand](https://zustand.docs.pmnd.rs/learn/getting-started/introduction) significantly simplify the use of Redux / RTK. However: -1. Still requires developers have their own methods for "objects-of-the-same-kind". -2. We still need to know the relationship between store vs. slice (`useBoundStore`). -3. I feel that the [`createBearFishSlice`](https://zustand.docs.pmnd.rs/learn/guides/slices-pattern#updating-multiple-stores) example is actually awkward. Why do we need to create additional slices if we want to update the states from multiple slices? +1. It still requires developers have their own methods for "objects-of-the-same-kind". +2. Developers still need to know the relationship between store vs. slice (`useBoundStore`). +3. With the [recommended single-store pattern](https://zustand.docs.pmnd.rs/learn/guides/flux-inspired-practice), the selectors can be confusing. For example, is `increment` is a selector for `useBearStore` or `useFishStore`? zustand appears to address this issue with [createSelectors](https://zustand.docs.pmnd.rs/learn/guides/auto-generating-selectors), which adds `.use.[selector]()` functions for accessing state properties and actions. +4. I feel that the [Bear and Fish example](https://zustand.docs.pmnd.rs/learn/guides/slices-pattern) is somewhat awkward because `Bear.eatFish()` assumes the existence of `state.fishes` and contaminates the `Fish` state. +5. I also feel that the [`createBearFishSlice`](https://zustand.docs.pmnd.rs/learn/guides/slices-pattern#updating-multiple-stores) example awkward. Why is it necessary to create an additional slice simply to update state across multiple slices? ## Modularized Thunk is All We Need diff --git a/doc-src/03-how-it-works.md b/doc-src/03-how-it-works.md index 48c1db02..c341dc76 100644 --- a/doc-src/03-how-it-works.md +++ b/doc-src/03-how-it-works.md @@ -1,6 +1,8 @@ # How It Works -## [ThunkModuleMap](https://github.com/chhsiao1981/use-thunk/blob/main/src/thunkContext/thunkModuleMap.ts): the Single Source of Truth +## [ThunkModuleMap](https://github.com/chhsiao1981/use-thunk/blob/main/src/thunkModule/thunkModuleMap.ts): the Single Source of Truth + +All the states are managed in a single source of truth: [ThunkModuleMap](https://github.com/chhsiao1981/use-thunk/blob/main/src/thunkModule/thunkModuleMap.ts). ## Object-`State` @@ -8,14 +10,26 @@ Object-states are typically used for component-presentation. Therefore, Object-s ## `ModuleState` and `NodeState` -We realized that developers mostly care only the object-states. `ModuleState` and `NodeState` are never renewed as new objects after each operations. This approach enables us to have `getStateByModule` to obtain the newest object-state while keeping object-states copy-on-write. +We realized that developers care only the object-states. `ModuleState`s are registered through [`registerThunk`](https://github.com/chhsiao1981/use-thunk/blob/main/src/registerThunk.ts) and never renewed as new objects after each operation. `NodeState`s are never renewed during [`update`](https://github.com/chhsiao1981/use-thunk/blob/main/src/defaultThunkFuncs/update.ts#L61) or [`upsert`](https://github.com/chhsiao1981/use-thunk/blob/main/src/defaultThunkFuncs/upsert.ts#L66). This approach enables us to have `getStateByModule` to obtain the newest object-state while keeping object-states copy-on-write. + +## Following Action-Dispatch-Reducer Pattern Under The Hood. + +Despite that we need only the thunk modules when using `use-thunk`, the implementation heavily utilizes action-dispatch-reducer pattern under the hood: + +* The implementation of `dispatch` can be found [here](https://github.com/chhsiao1981/use-thunk/blob/main/src/useThunk/useThunkReducer.ts#L50). +* [`getModuleState`](https://github.com/chhsiao1981/use-thunk/blob/main/src/useThunk/useThunkReducer.ts#L36) can be viewed as [the original `getState` in Redux Thunk](https://redux.js.org/usage/writing-logic-thunks). +* [`set`](https://github.com/chhsiao1981/use-thunk/blob/main/src/useThunk/useThunkReducer.ts#L98) is `dispatch` and the syntax sugar of `dispatch(upsert(id, data))`. +* [`get`](https://github.com/chhsiao1981/use-thunk/blob/main/src/useThunk/useThunkReducer.ts#L45) is the syntax sugar of getting the object-state from module state. +* [`getOrNull`](https://github.com/chhsiao1981/use-thunk/blob/main/src/useThunk/useThunkReducer.ts#L40) is the variation of `get`. + +## Reducers: [Only Primitive Reducers](https://github.com/chhsiao1981/use-thunk/blob/main/src/reducer/defaultReduceMap.ts) + +We recognize that state management requires only `init`, `get`, `update`, and `remove` ([CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete)). Furthermore, in most cases, only `upsert` and `get` are needed. Therefore, our implementation provides [only these primitive reducers](https://github.com/chhsiao1981/use-thunk/blob/main/src/reducer/defaultReduceMap.ts). ## Separation of `doModule` and `ModuleState` -Within a module, we realized that data and operations can be separated for easy maintenance, as there should be same module functions operating on different objects. Therefore, we have `doMod` to get the module functions, and `getMod` to get the module states. +Unlike the selector pattern used by RTK and Zustand, we believe that data and operations should be separated to improve maintainability, since the same module functions should be able to operate on different objects. Therefore, we provide [`doMod`](https://github.com/chhsiao1981/use-thunk/blob/main/src/thunkModule/doModule.ts#L69) for accessing module functions and [`getMod`](https://github.com/chhsiao1981/use-thunk/blob/main/src/thunkModule/thunkModuleMap.ts#L22) for accessing module state. ## Object-based Re-rendering -Starting 16.0.0, we use `useSyncExternalStore` for each object to achieve object-based re-rendering. - -## Reducer: Only Primitive Reducers +Starting 16.1.0, we use [`useSyncExternalStore`](https://github.com/chhsiao1981/use-thunk/blob/main/src/useThunk/useThunkReducer.ts#L23) for [each object](https://github.com/chhsiao1981/use-thunk/blob/main/src/states/node.ts#L41) to achieve object-based re-rendering. diff --git a/docs/00-introduction/index.html b/docs/00-introduction/index.html index b2162adb..e560f63d 100644 --- a/docs/00-introduction/index.html +++ b/docs/00-introduction/index.html @@ -651,9 +651,11 @@

Caveats of React Redux (an

Caveats of zustand

Recently zustand significantly simplify the use of Redux / RTK. However:

    -
  1. Still requires developers have their own methods for "objects-of-the-same-kind".
  2. -
  3. We still need to know the relationship between store vs. slice (useBoundStore).
  4. -
  5. I feel that the createBearFishSlice example is actually awkward. Why do we need to create additional slices if we want to update the states from multiple slices?
  6. +
  7. It still requires developers have their own methods for "objects-of-the-same-kind".
  8. +
  9. Developers still need to know the relationship between store vs. slice (useBoundStore).
  10. +
  11. With the recommended single-store pattern, the selectors can be confusing. For example, is increment is a selector for useBearStore or useFishStore? zustand appears to address this issue with createSelectors, which adds .use.[selector]() functions for accessing state properties and actions.
  12. +
  13. I feel that the Bear and Fish example is somewhat awkward because Bear.eatFish() assumes the existence of state.fishes and contaminates the Fish state.
  14. +
  15. I also feel that the createBearFishSlice example awkward. Why is it necessary to create an additional slice simply to update state across multiple slices?

Modularized Thunk is All We Need

React Redux, zustand, and many other GSM frameworks focus on: "We have stores (ideally a single store as single-source-of-truth) that manage the states. How do we manage the stores."

diff --git a/docs/03-how-it-works/index.html b/docs/03-how-it-works/index.html index fd970b46..50481687 100644 --- a/docs/03-how-it-works/index.html +++ b/docs/03-how-it-works/index.html @@ -421,10 +421,10 @@
  • - + - Separation of doModule and ModuleState + Following Action-Dispatch-Reducer Pattern Under The Hood. @@ -432,10 +432,10 @@
  • - + - Object-based Re-rendering + Reducers: Only Primitive Reducers @@ -443,10 +443,21 @@
  • - + - Reducer: Only Primitive Reducers + Separation of doModule and ModuleState + + + + +
  • + +
  • + + + + Object-based Re-rendering @@ -548,10 +559,10 @@
  • - + - Separation of doModule and ModuleState + Following Action-Dispatch-Reducer Pattern Under The Hood. @@ -559,10 +570,10 @@
  • - + - Object-based Re-rendering + Reducers: Only Primitive Reducers @@ -570,10 +581,21 @@
  • - + - Reducer: Only Primitive Reducers + Separation of doModule and ModuleState + + + + +
  • + +
  • + + + + Object-based Re-rendering @@ -601,16 +623,27 @@

    How It Works

    -

    ThunkModuleMap: the Single Source of Truth

    +

    ThunkModuleMap: the Single Source of Truth

    +

    All the states are managed in a single source of truth: ThunkModuleMap.

    Object-State

    Object-states are typically used for component-presentation. Therefore, Object-states require renew as new objects after each operation for ReactJS to detect the change of the state.

    ModuleState and NodeState

    -

    We realized that developers mostly care only the object-states. ModuleState and NodeState are never renewed as new objects after each operations. This approach enables us to have getStateByModule to obtain the newest object-state while keeping object-states copy-on-write.

    +

    We realized that developers care only the object-states. ModuleStates are registered through registerThunk and never renewed as new objects after each operation. NodeStates are never renewed during update or upsert. This approach enables us to have getStateByModule to obtain the newest object-state while keeping object-states copy-on-write.

    +

    Following Action-Dispatch-Reducer Pattern Under The Hood.

    +

    Despite that we need only the thunk modules when using use-thunk, the implementation heavily utilizes action-dispatch-reducer pattern under the hood:

    + +

    Reducers: Only Primitive Reducers

    +

    We recognize that state management requires only init, get, update, and remove (CRUD). Furthermore, in most cases, only upsert and get are needed. Therefore, our implementation provides only these primitive reducers.

    Separation of doModule and ModuleState

    -

    Within a module, we realized that data and operations can be separated for easy maintenance, as there should be same module functions operating on different objects. Therefore, we have doMod to get the module functions, and getMod to get the module states.

    +

    Unlike the selector pattern used by RTK and Zustand, we believe that data and operations should be separated to improve maintainability, since the same module functions should be able to operate on different objects. Therefore, we provide doMod for accessing module functions and getMod for accessing module state.

    Object-based Re-rendering

    -

    Starting 16.0.0, we use useSyncExternalStore for each object to achieve object-based re-rendering.

    -

    Reducer: Only Primitive Reducers

    +

    Starting 16.1.0, we use useSyncExternalStore for each object to achieve object-based re-rendering.

    diff --git a/docs/search/search_index.json b/docs/search/search_index.json index af3cb9e1..6f235559 100644 --- a/docs/search/search_index.json +++ b/docs/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"Getting Started","text":"

    use-thunk is a framework for easily managing global data state with useThunk, with zustand-like taste. Notably:

    use-thunk is inspired by the concepts of Redux Thunk and Redux Duck, with API naming inspired by zustand.

    For usage examples, please refer to demo-use-thunk (async counter) and demo-use-thunk-tic-tac-toe (cross-module communication).

    "},{"location":"#install","title":"Install","text":"
    npm install use-thunk\n
    "},{"location":"#getting-started_1","title":"Getting Started","text":""},{"location":"#id-based-usage","title":"id-based Usage","text":"

    A complete example to do increment:

    // thunks/increment.ts\nimport { type Thunk, type State as _State, update } from 'use-thunk'\n\nexport const name = 'demo/Increment'\n\nexport interface State extends _State {\n  count: number\n}\n\nexport const defaultState: State = {\n  count: 0\n}\n\n// upsert directly with set.\nexport const increment = (id: string, num: number = 1): Thunk<State> => {\n  return async (set, get) => {\n    const {count} = get(id)\n\n    set(id, { count: count + num })\n  }\n}\n\n// or we can treat set as dispatching a base action function (update).\nexport const increment2 = (id: string): Thunk<State> => {\n  return async (set, get) => {\n    const {count} = get(id)\n\n    set(update(id, { count: count + 2 }))\n  }\n}\n\n// or we can use set as dispatching a thunk function.\nexport const increment3 = (id: string): Thunk<State> => {\n  return async (set) => {\n    set(increment(id, 3))\n  }\n}\n
    // components/App.tsx\nimport { useThunk, getState, genID } from 'use-thunk'\nimport * as ModIncrement from './thunks/increment'\n\nexport default () => {\n  // or we can do:\n  // incrementID = genID()\n  // const [increment, doIncrement] = useThunk<ModIncrement.State, typeof ModIncrement>(ModIncrement, incrementID)\n  const [increment, doIncrement, incrementID] = useThunk<ModIncrement.State, typeof ModIncrement>(ModIncrement)\n\n  // to render\n  return (\n    <div>\n      <p>count: {increment.count}</p>\n      <button onClick={() => doIncrement.increment(incrementID)}>increase 1</button>\n      <button onClick={() => doIncrement.increment2(incrementID)}>increase 2</button>\n      <button onClick={() => doIncrement.increment3(incrementID)}>increase 3</button>\n    </div>\n  )\n}\n
    // main.tsx\nimport { registerThunk } from \"use-thunk\";\nimport { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport * as ModIncrement from './thunks/increment'\nimport App from \"./components/App\";\n\nregisterThunk(ModIncrement)\n\ncreateRoot(document.getElementById(\"root\")!).render(\n  <StrictMode>\n    <App />\n  </StrictMode>,\n)\n
    "},{"location":"#id-less-usage","title":"id-less Usage","text":"

    The id can be omitted if we have only 1 data-obj in the thunk module. For example, the previous increment example can be simplified as follow:

    // thunks/increment.ts\nimport { type Thunk, type State as _State, update } from 'use-thunk'\n\nexport const name = 'demo/Increment'\n\nexport interface State extends _State {\n  count: number\n}\n\nexport const defaultState: State = {\n  count: 0\n}\n\n// upsert directly with set.\nexport const increment = (num: number = 1): Thunk<State> => {\n  return async (set, get) => {\n    const {count} = get()\n\n    set(null, { count: count + num })\n  }\n}\n\n// or we can treat set as dispatching a base action function (update).\nexport const increment2 = (): Thunk<State> => {\n  return async (set, get) => {\n    const {count} = get()\n\n    set(update({ count: count + 2 }))\n  }\n}\n\n// or we can use set as dispatching a thunk function.\nexport const increment3 = (): Thunk<State> => {\n  return async (set) => {\n    set(increment(3))\n  }\n}\n
    // components/App.tsx\nimport { useThunk, getState } from 'use-thunk'\nimport * as ModIncrement from './thunks/increment'\n\nexport default () => {\n  const [increment, doIncrement] = useThunk<ModIncrement.State, typeof ModIncrement>(ModIncrement)\n\n  // to render\n  return (\n    <div>\n      <p>count: {increment.count}</p>\n      <button onClick={() => doIncrement.increment()}>increase 1</button>\n      <button onClick={() => doIncrement.increment2()}>increase 2</button>\n      <button onClick={() => doIncrement.increment3()}>increase 3</button>\n    </div>\n  )\n}\n
    // main.tsx\nimport { registerThunk } from \"use-thunk\";\nimport { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport * as ModIncrement from './thunks/increment'\nimport App from \"./components/App\";\n\nregisterThunk(ModIncrement)\n\ncreateRoot(document.getElementById(\"root\")!).render(\n  <StrictMode>\n    <App />\n  </StrictMode>,\n)\n
    "},{"location":"#development-pattern","title":"Development Pattern","text":""},{"location":"#must-included-in-a-thunk-module","title":"Must Included in a Thunk Module","text":"
    import type { State as _State } from 'use-thunk'\n\n// Thunk-module name.\nexport const name = \"\"\n\n// state definition of the reducer.\nexport interface State extends _State {\n}\n\nexport const defaultState: State = {}\n\nexport const func = (): Thunk<State> => {\n  return async (set, get) => {\n  }\n}\n\n.\n.\n.\n
    "},{"location":"#must-included-in-a-statically-allocated-always-allocated-component","title":"Must Included in a Statically-allocated (always allocated) Component","text":"
    import { useThunk, getState } from 'use-thunk'\nimport * as ModModule from '../thunks/module'\n\nconst Component = () => {\n  const [state, doModule] = useThunk<ModModule.State, typeof ModModule>(ModModule)\n\n.\n.\n.\n}\n
    "},{"location":"#must-included-in-maintsx","title":"Must Included in main.tsx","text":"
    import { registerThunk } from 'use-thunk'\nimport * as ModModule from '../thunks/module'\n\nregisterThunk(ModModule)\n.\n.\n.\n\ncreateRoot(document.getElementById(\"root\")!).render(\n  <StrictMode>\n    <App />\n  </StrictMode>,\n)\n
    "},{"location":"#updating-states","title":"Updating States","text":"

    Updating states follows the typical immutable-object scenario with shallow-eq. Therefore:

    We can also use other libraries (immer or immutable-js) to help us for immutable objects.

    "},{"location":"#async-functions","title":"Async Functions","text":"

    Similar to typical usage of thunk functions in React Redux, async functions / cancellation can be implemented within thunk functions:

    import type { State as _State, Thunk } from \"use-thunk\";\n\nexport interface State extends _State {\n  count: number;\n  value: number;\n  interval_ms: number;\n  abort?: AbortController;\n}\n\nexport const loop = (): Thunk<State> => {\n  return (set, get) => {\n    const { interval_ms, abort: preAbort } = get();\n    if (preAbort) {\n      preAbort.abort();\n    }\n\n    const abort = new AbortController();\n    set(null, { abort });\n\n    const theLoop = setInterval(() => {\n      console.info(\"parent.loop: now:\", new Date().getMilliseconds());\n      const { value, count } = get();\n      set(null, { value: value + count });\n    }, interval_ms);\n\n    abort.signal.addEventListener(\"abort\", () => {\n      clearInterval(theLoop);\n    });\n  };\n};\n

    Please check parent in demo-use-thunk for full implementation.

    "},{"location":"#acknowledgement","title":"Acknowledgement","text":""},{"location":"00-introduction/","title":"Introduction","text":""},{"location":"00-introduction/#global-state-management-gsm-in-reactjs","title":"Global State Management (GSM) in ReactJS","text":"

    ReactJS has been widely used since the introduction in 2014. ReactJS focuses on data presentation components and the local states of the React components. Since 16.8.0, ReactJS has drastically changed to function+hook styles, with useContext and useReducer (inspired by React Redux) as methods for global state management.

    There have been lots of data-management frameworks for ReactJS. Notably Dan Abramov and Andrew Clark's React Redux. React Redux introduced thunk (introduced in 2015) and many other impactful programming philosophy about GSM for ReactJS, significantly impacting many GSM frameworks.

    "},{"location":"00-introduction/#caveats-of-react-redux-and-redux-toolkit-rtk","title":"Caveats of React Redux (and Redux Toolkit (RTK))","text":"

    However, there are several caveats of React Redux / RTK:

    1. We need to know configureStore, createReducer, createSlice, and many other definitions before good usage of React Redux / RTK.
    2. Need to know relationship between action and reducer.
    3. Most of the definitions are in the createSlice functions. These functions can be complex gigantic functions in a complex app.
    4. Not really about \"objects-of-the-same-kind\" and requires developers have their own method. Redux recommends normalized states.
    5. Several redundant code when programming.
    "},{"location":"00-introduction/#caveats-of-zustand","title":"Caveats of zustand","text":"

    Recently zustand significantly simplify the use of Redux / RTK. However:

    1. Still requires developers have their own methods for \"objects-of-the-same-kind\".
    2. We still need to know the relationship between store vs. slice (useBoundStore).
    3. I feel that the createBearFishSlice example is actually awkward. Why do we need to create additional slices if we want to update the states from multiple slices?
    "},{"location":"00-introduction/#modularized-thunk-is-all-we-need","title":"Modularized Thunk is All We Need","text":"

    React Redux, zustand, and many other GSM frameworks focus on: \"We have stores (ideally a single store as single-source-of-truth) that manage the states. How do we manage the stores.\"

    Instead of focusing on the stores, use-thunk uses a different approach: \"We have objects that need to be managed. How do we group the objects to modules and manage the states of the objects through modularized operations.\" The modularized operations are implemented through thunks.

    "},{"location":"00-introduction/#goals-of-use-thunk","title":"Goals of use-thunk","text":"

    The primary objective of use-thunk is to streamline global state management in ReactJS by decoupling component rendering from business logic, eliminating boilerplate, and enforcing a highly maintainable, modular structure without the historical friction of React Redux.

    1. Separation of Concerns

    Following the foundational paradigm of React Redux, use-thunk enforces a strict separation between UI components and data management. Components focus entirely on layout and rendering, while business logic resides securely within decoupled domain modules.

    1. Unified Action-Reducer Architecture

      The traditional, verbose distinction between actions and reducers is eliminated:

      • Primitive Mutators (CUD): State mutations are restricted to built-in, predictable primitive operations (init, upsert, update, remove) that handle core data persistence.

      • Thunk Orchestrators: Action logic handles all computational overhead, side effects, and async flows, internally dispatching to the primitive mutators to update state.

    2. Modular Programming Paradigm

      The development experience is designed to mirror standard file-based module systems found in modern programming languages (e.g., Go, Python), rather than relying on complex Object-Oriented Programming (OOP) abstractions.

      • File-as-Module Structure: Developers write state logic as standard JavaScript/TypeScript modules. This approach eliminates OOP complexities like inheritance, polymorphism, and abstract factories in favor of pure, functional modularity.

      • Isolated Encapsulation: Each module governs its own distinct slice of the state. Cross-module state mutation is strictly forbidden; a module can only affect another module\u2019s state indirectly by invoking its exposed public functions.

      • Component Interface Simplicity: Components do not require a dispatch reference. They interact with state by directly invoking clean, module-scoped functions, minimizing inline data manipulation.

    3. Data Topography & Object Identification

      Data access and manipulation are designed to be intuitive, explicit, and safe:

      • Direct Object Representation (Read): When consuming data, the state is exposed directly as a native, immutable JavaScript/TypeScript object ({}), ensuring predictable copy-on-write behavior.

      • Discrete Entity Nodes: The module stores data as isolated, identifiable entity nodes. By utilizing explicit id parameters, operations are strictly scoped to a target entity, eliminating accidental collateral state updates.

      • Singleton Fallback: The id parameter in a module is entirely optional. When omitted, the module gracefully falls back with a uniquely identified default id (different for different modules).

    "},{"location":"00-introduction/#implementation","title":"Implementation","text":"

    To achieve the goals:

    1. Heavily use the concept of Thunk, to be able to have multiple computations/reductions in one operation.
    2. With the concept of \"normalized state\" in mind:
      • State: the state of each object.
      • NodeState: the metadata of each object, including the id and State.
      • ModuleState: the collection of NodeState in a module.
    3. The thunk functions are automatically attached with dispatch when used by the components. There is no need to use dispatch in components.
    4. API mainly exposes accessing module-based thunk functions, but reading only object-state-based data.
    5. Primitive thunk functions (init, upsert, update, remove) are implemented interally. The developers just call these primitive thunk functions to update the object-state.
    6. Use useSyncExternalStore to achieve object-state based re-rendering.
    7. Rename dispatch / getState to set / get / getOrNull / dispatch / getModuleState for extended and easier to use.
    "},{"location":"00-introduction/#primitive-thunk-functions","title":"Primitive Thunk Functions","text":"

    Primitive thunk functions are similar to the original actions in Redux.

    We provide the following default primitive thunk functions:

    "},{"location":"01-comparison/","title":"Comparison","text":"

    The following table is the comparison based on my knowledge:

    * not familiar with zustand and SSR.

    Items use-thunk React Redux *zustand useContext requiring single-store concept (can have only 1 create function) no yes (recommended) no modularized programming style natively built through createSlice through slice pattern not specified objects-of-the-same-module natively built no no no get state directly from get or useThunk as a js/ ts object through selectors through selectors directly from context value state operations directly from module functions through action / reducer through selectors as functions setValue in {value, setValue} pattern async functions within thunk functions within thunk functions within functions not specified requiring provider no yes no yes cross-module communication through doMod / getMod through dispatch(action) through creating new slice through setValue in {value, setValue} pattern knowledge requirement registerThunk / useThunk / thunk / thunk-module / primitive thunk functions (a lot) create / slice pattern / selector createContext / <Context /> / useContext / {value, setValue} pattern *server-side rendering (SSR) support (not tested) yes yes yes suitable usage all kinds of ReactJS apps, especially complex apps (ex: dashboard) all kinds of ReactJS apps all kinds of ReactJS apps simple ReactJS apps unless heavily customized"},{"location":"02-faq/","title":"FAQ","text":""},{"location":"02-faq/#is-it-another-redux-clone","title":"Is It Another Redux Clone?","text":"

    It's not a Redux clone: Despite the name, this isn't a Redux clone\u2014the underlying implementation is built on top of useSyncExternalStore. In addition:

    "},{"location":"02-faq/#since-it-is-not-redux-why-is-it-named-use-thunk","title":"Since It is Not Redux, Why is It Named use-thunk?","text":"
    1. The programming pattern is based on thunk.
    2. The library actually originated from github://nathanbuchar/react-hook-thunk-reducer, which is why \"thunk\" is in the name. I used it heavily in internal projects but struggled for a long time to find the right API naming. Recently, after seeing how intuitive zustand made things with set/get (as opposed to dispatch/getState), and combining that with the amazing feedback from r/reactjs on Reddit, I finally feel like the API is polished, clean, and ready for the public.
    "},{"location":"02-faq/#what-if-i-use-a-module-in-both-id-based-and-id-less","title":"What If I Use A Module In Both id-based and id-less\uff1f","text":"

    For the id-less object, we use genID (crypto.randomUUID) to generate an id for the id-less object. It is expected that the id-based objects and the id-less object would not interfere with each other.

    Therefore:

    1. It is expected that obj0 !== objDefault.

      const [obj0] = useThunk<MoModule.State, typeof ModModule>(id0)\nconst [objDefault] = useThunk<MoModule.State, typeof ModModule>()\n

    2. It is expected that the following useEffect updates no entity and considered bad programming style.

      const [obj0] = useThunk<MoModule.State, typeof ModModule>(id0)\n\nuseEffect(() => {\n    doModule.update({'test': 'test1'})\n}, [])\n

    3. example 2 can be re-written as:

      const [obj0] = useThunk<MoModule.State, typeof ModModule>()\n\nuseEffect(() => {\n    doModule.update({'test': 'test1'})\n}, [])\n

    4. example 2 can be re-written as (2):

      const [obj0, _, id0] = useThunk<MoModule.State, typeof ModModule>()\n\nuseEffect(() => {\n    doModule.update(id0, {'test': 'test1'})\n}, [])\n

    5. example 2 can be re-written as (3):

      const id0 = genID()\nconst [obj0] = useThunk<MoModule.State, typeof ModModule>(id0)\n\nuseEffect(() => {\n    doModule.update(id0, {'test': 'test1'})\n}, [])\n

    "},{"location":"03-how-it-works/","title":"How It Works","text":""},{"location":"03-how-it-works/#thunkmodulemap-the-single-source-of-truth","title":"ThunkModuleMap: the Single Source of Truth","text":""},{"location":"03-how-it-works/#object-state","title":"Object-State","text":"

    Object-states are typically used for component-presentation. Therefore, Object-states require renew as new objects after each operation for ReactJS to detect the change of the state.

    "},{"location":"03-how-it-works/#modulestate-and-nodestate","title":"ModuleState and NodeState","text":"

    We realized that developers mostly care only the object-states. ModuleState and NodeState are never renewed as new objects after each operations. This approach enables us to have getStateByModule to obtain the newest object-state while keeping object-states copy-on-write.

    "},{"location":"03-how-it-works/#separation-of-domodule-and-modulestate","title":"Separation of doModule and ModuleState","text":"

    Within a module, we realized that data and operations can be separated for easy maintenance, as there should be same module functions operating on different objects. Therefore, we have doMod to get the module functions, and getMod to get the module states.

    "},{"location":"03-how-it-works/#object-based-re-rendering","title":"Object-based Re-rendering","text":"

    Starting 16.0.0, we use useSyncExternalStore for each object to achieve object-based re-rendering.

    "},{"location":"03-how-it-works/#reducer-only-primitive-reducers","title":"Reducer: Only Primitive Reducers","text":""},{"location":"04-apis/","title":"APIs","text":""},{"location":"04-apis/#types","title":"Types","text":""},{"location":"04-apis/#type-state","title":"type State","text":"
    export interface State {\n  [key: string]: unknown\n}\n

    State is the most fundamental type for the states in ThunkModules.

    "},{"location":"04-apis/#type-thunkmodules-extends-state","title":"type ThunkModule<S extends State>","text":"
    export type ThunkModule<S extends State> = {\n  name: string // module name. convention: [project-name]/[module].\n  defaultState: S // default state.\n\n  // The rest of the variables are doModule.\n  // Specifying index-signatures to include all the variables.\n  [action: string]: ThunkFunc<S> | string | S\n}\n

    A ThunkModule represents a self-contained domain state slice implemented within a single file. It encapsulates the module's identity, its initial data structure, and the business logic workflows (thunk functions) that act upon it.

    "},{"location":"04-apis/#type-thunkfuncs-extends-state","title":"type ThunkFunc<S extends State>","text":"
    export type ThunkFunc<S extends State> = (...params: any[]) => Thunk<S>\n

    A thunk function in a thunk module. Thunk function is a function returning thunk.

    "},{"location":"04-apis/#type-thunk","title":"type Thunk","text":"

    Primitively, Thunk is defined as:

    export type Thunk<S extends State> = async (\n  set: (actionOrID: ThunkFunc | string | null | undefined, data?: Partial<S>) => void,\n  get: (id?: string | null) => S,\n) => void\n

    Thunks can be async functions if needed (ex: fetch data).

    Full definition of Thunk is in the Advanced Usage section.

    "},{"location":"04-apis/#registerthunk-usethunk","title":"RegisterThunk / useThunk","text":""},{"location":"04-apis/#registerthunkmodule","title":"registerThunk(module)","text":"

    const registerThunk = <S extends State>(module: ThunkModule<S>) => void\n
    Register a thunk module.

    "},{"location":"04-apis/#usethunkmodule","title":"useThunk(module)","text":"
    const useThunk = <S extends State, T extends ThunkModule<S>>(module: T, id?: string) => [state: Readonly<S>, doModule: doModule<S, T>, string]\n

    [Guaranteed] Get the state of the id, doModule, and the id. Use ensured defaultID if id is not present. Create a state with defaultState in moduleState if state does not exist.

    return: [state, doModule, id].

    "},{"location":"04-apis/#module-related","title":"Module Related","text":""},{"location":"04-apis/#domodmodulename","title":"doMod(moduleName)","text":"
    const doMod = <S extends State, T extends ThunkModule<S>>(moduleName: string): doModule<S, T>\n

    Get the module operators/functions by module name.

    "},{"location":"04-apis/#getmodmodulename","title":"getMod(moduleName)","text":"
    const getMod = <S extends State>(moduleName: string): Readonly<ModuleState<S>>\n

    Get the module state by module name.

    "},{"location":"04-apis/#primitive-thunk-functions","title":"Primitive Thunk Functions","text":""},{"location":"04-apis/#upsertidordata-data","title":"upsert(idOrData, data?)","text":"
    const upsert = <S extends State>(\n  idOrData: Partial<S> | string | null | undefined,\n  data?: Partial<S>,\n): Thunk<S>\n

    [Guaranteed] Update the data. Create a state with defaultState in moduleState if state does not exist.

    Can be used as:

    "},{"location":"04-apis/#updateidordata-data","title":"update(idOrData, data?)","text":"
    const update = <S extends State>(\n  idOrData: Partial<S> | string | null | undefined,\n  data?: Partial<S>,\n): Thunk<S>\n

    Update the data. No update if id or data is invalid.

    Can be used as:

    "},{"location":"04-apis/#removeid","title":"remove(id?)","text":"
    const remove = <S extends State>(id?: string | null): Thunk<S>\n

    Remove the state. Use defaultID if id is not specified.

    "},{"location":"04-apis/#initidorstate-state","title":"init(idOrState?, state?)","text":"
    const init = <S extends State>(\n  idOrState?: S | string | null | undefined,\n  state?: S,\n): Thunk<S>\n

    [Guaranteed] Initialize the state. Use ensured defaultID if id is not present. Create a state with defaultState in moduleState if state is not specified.

    Most of time we don't need to init because upsert, set(id, data), get(id) and useThunk automatically initialize the state if not exist.

    "},{"location":"04-apis/#misc","title":"Misc","text":""},{"location":"04-apis/#genidcustomgenid","title":"genID(customGenID?)","text":"
    const genID = (customGenID?: () => string): string\n

    Generate id for the state. Default mechanism: crypto.randomUUID.

    "},{"location":"04-apis/#advanced-usage","title":"Advanced Usage","text":"

    The following APIs are for advanced usage.

    "},{"location":"04-apis/#types_1","title":"types","text":""},{"location":"04-apis/#type-thunk_1","title":"type Thunk","text":"

    Full definition of Thunk is:

    export type Thunk<S extends State> = async (\n  set: (actionOrID: ThunkFunc | string | null | undefined, data?: Partial<S>) => void,\n  get: (id?: string) => S,\n  getOrNull: (id?: string) => S | null | undefined,\n  dispatch: Dispatch<ActionOrThunk<S>>,\n  getModuleState: () => ModuleState<S>,\n) => void\n
    "},{"location":"04-apis/#type-usethunk","title":"type UseThunk","text":"
    type UseThunk<S extends State, T extends ThunkModule<S>> = [Readonly<S>, toDoModule<S, T>, string]\n
    "},{"location":"04-apis/#type-domodule","title":"type doModule","text":"
    type doModule<S extends State, T extends ThunkModule<S>> = {\n  // @ts-expect-error toThunkFuncMap includes only ThunkFunc<S> | BaseActionFunc\n  [action in keyof toThunkFuncMap<T>]: VoidReturnType<toThunkFuncMap<T>[action]>\n} & Omit<defaultDoModule, keyof toThunkFuncMap<T>>\n

    Functions in doModule already wrap thunk functions with set (dispatch in Redux / useReducer). doModule functions can be directly used. We don't wrap doModule functions with set/dispatch.

    "},{"location":"04-apis/#type-modulestate","title":"type ModuleState","text":"
    type ModuleState<S extends State> = {\n  name: string\n  nodes: NodeStateMap<S>\n  defaultState: S\n  defaultID?: string | null\n}\n
    "},{"location":"04-apis/#type-customgenid","title":"type CustomGenID","text":"
    type CustomGenID = () => string\n

    Module state.

    "},{"location":"04-apis/#primitive-thunk-functions_1","title":"Primitive Thunk Functions","text":""},{"location":"04-apis/#setdefaultidid-string","title":"setDefaultID(id: string)","text":"
    const setDefaultID = (id): BaseAction\n

    Set default id in module state.

    "},{"location":"04-apis/#module-state-related","title":"Module State Related","text":""},{"location":"04-apis/#getstatebymodulemodulestate-id","title":"getStateByModule(moduleState, id?)","text":"
    const getStateByModule = <S extends State>(\n  moduleState: ModuleState<S>,\n  id?: string | null,\n): Readonly<S>\n

    [Guaranteed] Get the state from module state. id as ensured defaultID if id is not present. Create a state with defaultState in moduleState if state does not exist.

    [NOTICE] Used only within thunks or event handles and effect hooks in components. Use useThunk in component rendering.

    "},{"location":"04-apis/#getstateornullbymodulemodulestate-id","title":"getStateOrNullByModule(moduleState, id?)","text":"
    const getStateOrNullByModule = <S extends State>(\n  moduleState: ModuleState<S>,\n  id?: string | null,\n): Readonly<S | null>\n

    Get the state from module state. Return null if the state does not exist.

    "},{"location":"04-apis/#getnodeornullbymodulemodulestate-id","title":"getNodeOrNullByModule(moduleState, id?)","text":"
    const getNodeOrNullByModule = <S extends State>(\n  moduleState: ModuleState<S>,\n  id?: string | null,\n): Readonly<NodeState<S> | null>\n

    Get the node from module state. Return null if the node does not exist.

    "},{"location":"04-apis/#getdefaultidmodulestate","title":"getDefaultID(modulestate)","text":"
    const getDefaultID = <S extends State>(moduleState: ModuleState<S>): string | null | undefined\n

    Get defaultID.

    "}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"Getting Started","text":"

    use-thunk is a framework for easily managing global data state with useThunk, with zustand-like taste. Notably:

    use-thunk is inspired by the concepts of Redux Thunk and Redux Duck, with API naming inspired by zustand.

    For usage examples, please refer to demo-use-thunk (async counter) and demo-use-thunk-tic-tac-toe (cross-module communication).

    "},{"location":"#install","title":"Install","text":"
    npm install use-thunk\n
    "},{"location":"#getting-started_1","title":"Getting Started","text":""},{"location":"#id-based-usage","title":"id-based Usage","text":"

    A complete example to do increment:

    // thunks/increment.ts\nimport { type Thunk, type State as _State, update } from 'use-thunk'\n\nexport const name = 'demo/Increment'\n\nexport interface State extends _State {\n  count: number\n}\n\nexport const defaultState: State = {\n  count: 0\n}\n\n// upsert directly with set.\nexport const increment = (id: string, num: number = 1): Thunk<State> => {\n  return async (set, get) => {\n    const {count} = get(id)\n\n    set(id, { count: count + num })\n  }\n}\n\n// or we can treat set as dispatching a base action function (update).\nexport const increment2 = (id: string): Thunk<State> => {\n  return async (set, get) => {\n    const {count} = get(id)\n\n    set(update(id, { count: count + 2 }))\n  }\n}\n\n// or we can use set as dispatching a thunk function.\nexport const increment3 = (id: string): Thunk<State> => {\n  return async (set) => {\n    set(increment(id, 3))\n  }\n}\n
    // components/App.tsx\nimport { useThunk, getState, genID } from 'use-thunk'\nimport * as ModIncrement from './thunks/increment'\n\nexport default () => {\n  // or we can do:\n  // incrementID = genID()\n  // const [increment, doIncrement] = useThunk<ModIncrement.State, typeof ModIncrement>(ModIncrement, incrementID)\n  const [increment, doIncrement, incrementID] = useThunk<ModIncrement.State, typeof ModIncrement>(ModIncrement)\n\n  // to render\n  return (\n    <div>\n      <p>count: {increment.count}</p>\n      <button onClick={() => doIncrement.increment(incrementID)}>increase 1</button>\n      <button onClick={() => doIncrement.increment2(incrementID)}>increase 2</button>\n      <button onClick={() => doIncrement.increment3(incrementID)}>increase 3</button>\n    </div>\n  )\n}\n
    // main.tsx\nimport { registerThunk } from \"use-thunk\";\nimport { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport * as ModIncrement from './thunks/increment'\nimport App from \"./components/App\";\n\nregisterThunk(ModIncrement)\n\ncreateRoot(document.getElementById(\"root\")!).render(\n  <StrictMode>\n    <App />\n  </StrictMode>,\n)\n
    "},{"location":"#id-less-usage","title":"id-less Usage","text":"

    The id can be omitted if we have only 1 data-obj in the thunk module. For example, the previous increment example can be simplified as follow:

    // thunks/increment.ts\nimport { type Thunk, type State as _State, update } from 'use-thunk'\n\nexport const name = 'demo/Increment'\n\nexport interface State extends _State {\n  count: number\n}\n\nexport const defaultState: State = {\n  count: 0\n}\n\n// upsert directly with set.\nexport const increment = (num: number = 1): Thunk<State> => {\n  return async (set, get) => {\n    const {count} = get()\n\n    set(null, { count: count + num })\n  }\n}\n\n// or we can treat set as dispatching a base action function (update).\nexport const increment2 = (): Thunk<State> => {\n  return async (set, get) => {\n    const {count} = get()\n\n    set(update({ count: count + 2 }))\n  }\n}\n\n// or we can use set as dispatching a thunk function.\nexport const increment3 = (): Thunk<State> => {\n  return async (set) => {\n    set(increment(3))\n  }\n}\n
    // components/App.tsx\nimport { useThunk, getState } from 'use-thunk'\nimport * as ModIncrement from './thunks/increment'\n\nexport default () => {\n  const [increment, doIncrement] = useThunk<ModIncrement.State, typeof ModIncrement>(ModIncrement)\n\n  // to render\n  return (\n    <div>\n      <p>count: {increment.count}</p>\n      <button onClick={() => doIncrement.increment()}>increase 1</button>\n      <button onClick={() => doIncrement.increment2()}>increase 2</button>\n      <button onClick={() => doIncrement.increment3()}>increase 3</button>\n    </div>\n  )\n}\n
    // main.tsx\nimport { registerThunk } from \"use-thunk\";\nimport { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport * as ModIncrement from './thunks/increment'\nimport App from \"./components/App\";\n\nregisterThunk(ModIncrement)\n\ncreateRoot(document.getElementById(\"root\")!).render(\n  <StrictMode>\n    <App />\n  </StrictMode>,\n)\n
    "},{"location":"#development-pattern","title":"Development Pattern","text":""},{"location":"#must-included-in-a-thunk-module","title":"Must Included in a Thunk Module","text":"
    import type { State as _State } from 'use-thunk'\n\n// Thunk-module name.\nexport const name = \"\"\n\n// state definition of the reducer.\nexport interface State extends _State {\n}\n\nexport const defaultState: State = {}\n\nexport const func = (): Thunk<State> => {\n  return async (set, get) => {\n  }\n}\n\n.\n.\n.\n
    "},{"location":"#must-included-in-a-statically-allocated-always-allocated-component","title":"Must Included in a Statically-allocated (always allocated) Component","text":"
    import { useThunk, getState } from 'use-thunk'\nimport * as ModModule from '../thunks/module'\n\nconst Component = () => {\n  const [state, doModule] = useThunk<ModModule.State, typeof ModModule>(ModModule)\n\n.\n.\n.\n}\n
    "},{"location":"#must-included-in-maintsx","title":"Must Included in main.tsx","text":"
    import { registerThunk } from 'use-thunk'\nimport * as ModModule from '../thunks/module'\n\nregisterThunk(ModModule)\n.\n.\n.\n\ncreateRoot(document.getElementById(\"root\")!).render(\n  <StrictMode>\n    <App />\n  </StrictMode>,\n)\n
    "},{"location":"#updating-states","title":"Updating States","text":"

    Updating states follows the typical immutable-object scenario with shallow-eq. Therefore:

    We can also use other libraries (immer or immutable-js) to help us for immutable objects.

    "},{"location":"#async-functions","title":"Async Functions","text":"

    Similar to typical usage of thunk functions in React Redux, async functions / cancellation can be implemented within thunk functions:

    import type { State as _State, Thunk } from \"use-thunk\";\n\nexport interface State extends _State {\n  count: number;\n  value: number;\n  interval_ms: number;\n  abort?: AbortController;\n}\n\nexport const loop = (): Thunk<State> => {\n  return (set, get) => {\n    const { interval_ms, abort: preAbort } = get();\n    if (preAbort) {\n      preAbort.abort();\n    }\n\n    const abort = new AbortController();\n    set(null, { abort });\n\n    const theLoop = setInterval(() => {\n      console.info(\"parent.loop: now:\", new Date().getMilliseconds());\n      const { value, count } = get();\n      set(null, { value: value + count });\n    }, interval_ms);\n\n    abort.signal.addEventListener(\"abort\", () => {\n      clearInterval(theLoop);\n    });\n  };\n};\n

    Please check parent in demo-use-thunk for full implementation.

    "},{"location":"#acknowledgement","title":"Acknowledgement","text":""},{"location":"00-introduction/","title":"Introduction","text":""},{"location":"00-introduction/#global-state-management-gsm-in-reactjs","title":"Global State Management (GSM) in ReactJS","text":"

    ReactJS has been widely used since the introduction in 2014. ReactJS focuses on data presentation components and the local states of the React components. Since 16.8.0, ReactJS has drastically changed to function+hook styles, with useContext and useReducer (inspired by React Redux) as methods for global state management.

    There have been lots of data-management frameworks for ReactJS. Notably Dan Abramov and Andrew Clark's React Redux. React Redux introduced thunk (introduced in 2015) and many other impactful programming philosophy about GSM for ReactJS, significantly impacting many GSM frameworks.

    "},{"location":"00-introduction/#caveats-of-react-redux-and-redux-toolkit-rtk","title":"Caveats of React Redux (and Redux Toolkit (RTK))","text":"

    However, there are several caveats of React Redux / RTK:

    1. We need to know configureStore, createReducer, createSlice, and many other definitions before good usage of React Redux / RTK.
    2. Need to know relationship between action and reducer.
    3. Most of the definitions are in the createSlice functions. These functions can be complex gigantic functions in a complex app.
    4. Not really about \"objects-of-the-same-kind\" and requires developers have their own method. Redux recommends normalized states.
    5. Several redundant code when programming.
    "},{"location":"00-introduction/#caveats-of-zustand","title":"Caveats of zustand","text":"

    Recently zustand significantly simplify the use of Redux / RTK. However:

    1. It still requires developers have their own methods for \"objects-of-the-same-kind\".
    2. Developers still need to know the relationship between store vs. slice (useBoundStore).
    3. With the recommended single-store pattern, the selectors can be confusing. For example, is increment is a selector for useBearStore or useFishStore? zustand appears to address this issue with createSelectors, which adds .use.[selector]() functions for accessing state properties and actions.
    4. I feel that the Bear and Fish example is somewhat awkward because Bear.eatFish() assumes the existence of state.fishes and contaminates the Fish state.
    5. I also feel that the createBearFishSlice example awkward. Why is it necessary to create an additional slice simply to update state across multiple slices?
    "},{"location":"00-introduction/#modularized-thunk-is-all-we-need","title":"Modularized Thunk is All We Need","text":"

    React Redux, zustand, and many other GSM frameworks focus on: \"We have stores (ideally a single store as single-source-of-truth) that manage the states. How do we manage the stores.\"

    Instead of focusing on the stores, use-thunk uses a different approach: \"We have objects that need to be managed. How do we group the objects to modules and manage the states of the objects through modularized operations.\" The modularized operations are implemented through thunks.

    "},{"location":"00-introduction/#goals-of-use-thunk","title":"Goals of use-thunk","text":"

    The primary objective of use-thunk is to streamline global state management in ReactJS by decoupling component rendering from business logic, eliminating boilerplate, and enforcing a highly maintainable, modular structure without the historical friction of React Redux.

    1. Separation of Concerns

    Following the foundational paradigm of React Redux, use-thunk enforces a strict separation between UI components and data management. Components focus entirely on layout and rendering, while business logic resides securely within decoupled domain modules.

    1. Unified Action-Reducer Architecture

      The traditional, verbose distinction between actions and reducers is eliminated:

      • Primitive Mutators (CUD): State mutations are restricted to built-in, predictable primitive operations (init, upsert, update, remove) that handle core data persistence.

      • Thunk Orchestrators: Action logic handles all computational overhead, side effects, and async flows, internally dispatching to the primitive mutators to update state.

    2. Modular Programming Paradigm

      The development experience is designed to mirror standard file-based module systems found in modern programming languages (e.g., Go, Python), rather than relying on complex Object-Oriented Programming (OOP) abstractions.

      • File-as-Module Structure: Developers write state logic as standard JavaScript/TypeScript modules. This approach eliminates OOP complexities like inheritance, polymorphism, and abstract factories in favor of pure, functional modularity.

      • Isolated Encapsulation: Each module governs its own distinct slice of the state. Cross-module state mutation is strictly forbidden; a module can only affect another module\u2019s state indirectly by invoking its exposed public functions.

      • Component Interface Simplicity: Components do not require a dispatch reference. They interact with state by directly invoking clean, module-scoped functions, minimizing inline data manipulation.

    3. Data Topography & Object Identification

      Data access and manipulation are designed to be intuitive, explicit, and safe:

      • Direct Object Representation (Read): When consuming data, the state is exposed directly as a native, immutable JavaScript/TypeScript object ({}), ensuring predictable copy-on-write behavior.

      • Discrete Entity Nodes: The module stores data as isolated, identifiable entity nodes. By utilizing explicit id parameters, operations are strictly scoped to a target entity, eliminating accidental collateral state updates.

      • Singleton Fallback: The id parameter in a module is entirely optional. When omitted, the module gracefully falls back with a uniquely identified default id (different for different modules).

    "},{"location":"00-introduction/#implementation","title":"Implementation","text":"

    To achieve the goals:

    1. Heavily use the concept of Thunk, to be able to have multiple computations/reductions in one operation.
    2. With the concept of \"normalized state\" in mind:
      • State: the state of each object.
      • NodeState: the metadata of each object, including the id and State.
      • ModuleState: the collection of NodeState in a module.
    3. The thunk functions are automatically attached with dispatch when used by the components. There is no need to use dispatch in components.
    4. API mainly exposes accessing module-based thunk functions, but reading only object-state-based data.
    5. Primitive thunk functions (init, upsert, update, remove) are implemented interally. The developers just call these primitive thunk functions to update the object-state.
    6. Use useSyncExternalStore to achieve object-state based re-rendering.
    7. Rename dispatch / getState to set / get / getOrNull / dispatch / getModuleState for extended and easier to use.
    "},{"location":"00-introduction/#primitive-thunk-functions","title":"Primitive Thunk Functions","text":"

    Primitive thunk functions are similar to the original actions in Redux.

    We provide the following default primitive thunk functions:

    "},{"location":"01-comparison/","title":"Comparison","text":"

    The following table is the comparison based on my knowledge:

    * not familiar with zustand and SSR.

    Items use-thunk React Redux *zustand useContext requiring single-store concept (can have only 1 create function) no yes (recommended) no modularized programming style natively built through createSlice through slice pattern not specified objects-of-the-same-module natively built no no no get state directly from get or useThunk as a js/ ts object through selectors through selectors directly from context value state operations directly from module functions through action / reducer through selectors as functions setValue in {value, setValue} pattern async functions within thunk functions within thunk functions within functions not specified requiring provider no yes no yes cross-module communication through doMod / getMod through dispatch(action) through creating new slice through setValue in {value, setValue} pattern knowledge requirement registerThunk / useThunk / thunk / thunk-module / primitive thunk functions (a lot) create / slice pattern / selector createContext / <Context /> / useContext / {value, setValue} pattern *server-side rendering (SSR) support (not tested) yes yes yes suitable usage all kinds of ReactJS apps, especially complex apps (ex: dashboard) all kinds of ReactJS apps all kinds of ReactJS apps simple ReactJS apps unless heavily customized"},{"location":"02-faq/","title":"FAQ","text":""},{"location":"02-faq/#is-it-another-redux-clone","title":"Is It Another Redux Clone?","text":"

    It's not a Redux clone: Despite the name, this isn't a Redux clone\u2014the underlying implementation is built on top of useSyncExternalStore. In addition:

    "},{"location":"02-faq/#since-it-is-not-redux-why-is-it-named-use-thunk","title":"Since It is Not Redux, Why is It Named use-thunk?","text":"
    1. The programming pattern is based on thunk.
    2. The library actually originated from github://nathanbuchar/react-hook-thunk-reducer, which is why \"thunk\" is in the name. I used it heavily in internal projects but struggled for a long time to find the right API naming. Recently, after seeing how intuitive zustand made things with set/get (as opposed to dispatch/getState), and combining that with the amazing feedback from r/reactjs on Reddit, I finally feel like the API is polished, clean, and ready for the public.
    "},{"location":"02-faq/#what-if-i-use-a-module-in-both-id-based-and-id-less","title":"What If I Use A Module In Both id-based and id-less\uff1f","text":"

    For the id-less object, we use genID (crypto.randomUUID) to generate an id for the id-less object. It is expected that the id-based objects and the id-less object would not interfere with each other.

    Therefore:

    1. It is expected that obj0 !== objDefault.

      const [obj0] = useThunk<MoModule.State, typeof ModModule>(id0)\nconst [objDefault] = useThunk<MoModule.State, typeof ModModule>()\n

    2. It is expected that the following useEffect updates no entity and considered bad programming style.

      const [obj0] = useThunk<MoModule.State, typeof ModModule>(id0)\n\nuseEffect(() => {\n    doModule.update({'test': 'test1'})\n}, [])\n

    3. example 2 can be re-written as:

      const [obj0] = useThunk<MoModule.State, typeof ModModule>()\n\nuseEffect(() => {\n    doModule.update({'test': 'test1'})\n}, [])\n

    4. example 2 can be re-written as (2):

      const [obj0, _, id0] = useThunk<MoModule.State, typeof ModModule>()\n\nuseEffect(() => {\n    doModule.update(id0, {'test': 'test1'})\n}, [])\n

    5. example 2 can be re-written as (3):

      const id0 = genID()\nconst [obj0] = useThunk<MoModule.State, typeof ModModule>(id0)\n\nuseEffect(() => {\n    doModule.update(id0, {'test': 'test1'})\n}, [])\n

    "},{"location":"03-how-it-works/","title":"How It Works","text":""},{"location":"03-how-it-works/#thunkmodulemap-the-single-source-of-truth","title":"ThunkModuleMap: the Single Source of Truth","text":"

    All the states are managed in a single source of truth: ThunkModuleMap.

    "},{"location":"03-how-it-works/#object-state","title":"Object-State","text":"

    Object-states are typically used for component-presentation. Therefore, Object-states require renew as new objects after each operation for ReactJS to detect the change of the state.

    "},{"location":"03-how-it-works/#modulestate-and-nodestate","title":"ModuleState and NodeState","text":"

    We realized that developers care only the object-states. ModuleStates are registered through registerThunk and never renewed as new objects after each operation. NodeStates are never renewed during update or upsert. This approach enables us to have getStateByModule to obtain the newest object-state while keeping object-states copy-on-write.

    "},{"location":"03-how-it-works/#following-action-dispatch-reducer-pattern-under-the-hood","title":"Following Action-Dispatch-Reducer Pattern Under The Hood.","text":"

    Despite that we need only the thunk modules when using use-thunk, the implementation heavily utilizes action-dispatch-reducer pattern under the hood:

    "},{"location":"03-how-it-works/#reducers-only-primitive-reducers","title":"Reducers: Only Primitive Reducers","text":"

    We recognize that state management requires only init, get, update, and remove (CRUD). Furthermore, in most cases, only upsert and get are needed. Therefore, our implementation provides only these primitive reducers.

    "},{"location":"03-how-it-works/#separation-of-domodule-and-modulestate","title":"Separation of doModule and ModuleState","text":"

    Unlike the selector pattern used by RTK and Zustand, we believe that data and operations should be separated to improve maintainability, since the same module functions should be able to operate on different objects. Therefore, we provide doMod for accessing module functions and getMod for accessing module state.

    "},{"location":"03-how-it-works/#object-based-re-rendering","title":"Object-based Re-rendering","text":"

    Starting 16.1.0, we use useSyncExternalStore for each object to achieve object-based re-rendering.

    "},{"location":"04-apis/","title":"APIs","text":""},{"location":"04-apis/#types","title":"Types","text":""},{"location":"04-apis/#type-state","title":"type State","text":"
    export interface State {\n  [key: string]: unknown\n}\n

    State is the most fundamental type for the states in ThunkModules.

    "},{"location":"04-apis/#type-thunkmodules-extends-state","title":"type ThunkModule<S extends State>","text":"
    export type ThunkModule<S extends State> = {\n  name: string // module name. convention: [project-name]/[module].\n  defaultState: S // default state.\n\n  // The rest of the variables are doModule.\n  // Specifying index-signatures to include all the variables.\n  [action: string]: ThunkFunc<S> | string | S\n}\n

    A ThunkModule represents a self-contained domain state slice implemented within a single file. It encapsulates the module's identity, its initial data structure, and the business logic workflows (thunk functions) that act upon it.

    "},{"location":"04-apis/#type-thunkfuncs-extends-state","title":"type ThunkFunc<S extends State>","text":"
    export type ThunkFunc<S extends State> = (...params: any[]) => Thunk<S>\n

    A thunk function in a thunk module. Thunk function is a function returning thunk.

    "},{"location":"04-apis/#type-thunk","title":"type Thunk","text":"

    Primitively, Thunk is defined as:

    export type Thunk<S extends State> = async (\n  set: (actionOrID: ThunkFunc | string | null | undefined, data?: Partial<S>) => void,\n  get: (id?: string | null) => S,\n) => void\n

    Thunks can be async functions if needed (ex: fetch data).

    Full definition of Thunk is in the Advanced Usage section.

    "},{"location":"04-apis/#registerthunk-usethunk","title":"RegisterThunk / useThunk","text":""},{"location":"04-apis/#registerthunkmodule","title":"registerThunk(module)","text":"

    const registerThunk = <S extends State>(module: ThunkModule<S>) => void\n
    Register a thunk module.

    "},{"location":"04-apis/#usethunkmodule","title":"useThunk(module)","text":"
    const useThunk = <S extends State, T extends ThunkModule<S>>(module: T, id?: string) => [state: Readonly<S>, doModule: doModule<S, T>, string]\n

    [Guaranteed] Get the state of the id, doModule, and the id. Use ensured defaultID if id is not present. Create a state with defaultState in moduleState if state does not exist.

    return: [state, doModule, id].

    "},{"location":"04-apis/#module-related","title":"Module Related","text":""},{"location":"04-apis/#domodmodulename","title":"doMod(moduleName)","text":"
    const doMod = <S extends State, T extends ThunkModule<S>>(moduleName: string): doModule<S, T>\n

    Get the module operators/functions by module name.

    "},{"location":"04-apis/#getmodmodulename","title":"getMod(moduleName)","text":"
    const getMod = <S extends State>(moduleName: string): Readonly<ModuleState<S>>\n

    Get the module state by module name.

    "},{"location":"04-apis/#primitive-thunk-functions","title":"Primitive Thunk Functions","text":""},{"location":"04-apis/#upsertidordata-data","title":"upsert(idOrData, data?)","text":"
    const upsert = <S extends State>(\n  idOrData: Partial<S> | string | null | undefined,\n  data?: Partial<S>,\n): Thunk<S>\n

    [Guaranteed] Update the data. Create a state with defaultState in moduleState if state does not exist.

    Can be used as:

    "},{"location":"04-apis/#updateidordata-data","title":"update(idOrData, data?)","text":"
    const update = <S extends State>(\n  idOrData: Partial<S> | string | null | undefined,\n  data?: Partial<S>,\n): Thunk<S>\n

    Update the data. No update if id or data is invalid.

    Can be used as:

    "},{"location":"04-apis/#removeid","title":"remove(id?)","text":"
    const remove = <S extends State>(id?: string | null): Thunk<S>\n

    Remove the state. Use defaultID if id is not specified.

    "},{"location":"04-apis/#initidorstate-state","title":"init(idOrState?, state?)","text":"
    const init = <S extends State>(\n  idOrState?: S | string | null | undefined,\n  state?: S,\n): Thunk<S>\n

    [Guaranteed] Initialize the state. Use ensured defaultID if id is not present. Create a state with defaultState in moduleState if state is not specified.

    Most of time we don't need to init because upsert, set(id, data), get(id) and useThunk automatically initialize the state if not exist.

    "},{"location":"04-apis/#misc","title":"Misc","text":""},{"location":"04-apis/#genidcustomgenid","title":"genID(customGenID?)","text":"
    const genID = (customGenID?: () => string): string\n

    Generate id for the state. Default mechanism: crypto.randomUUID.

    "},{"location":"04-apis/#advanced-usage","title":"Advanced Usage","text":"

    The following APIs are for advanced usage.

    "},{"location":"04-apis/#types_1","title":"types","text":""},{"location":"04-apis/#type-thunk_1","title":"type Thunk","text":"

    Full definition of Thunk is:

    export type Thunk<S extends State> = async (\n  set: (actionOrID: ThunkFunc | string | null | undefined, data?: Partial<S>) => void,\n  get: (id?: string) => S,\n  getOrNull: (id?: string) => S | null | undefined,\n  dispatch: Dispatch<ActionOrThunk<S>>,\n  getModuleState: () => ModuleState<S>,\n) => void\n
    "},{"location":"04-apis/#type-usethunk","title":"type UseThunk","text":"
    type UseThunk<S extends State, T extends ThunkModule<S>> = [Readonly<S>, toDoModule<S, T>, string]\n
    "},{"location":"04-apis/#type-domodule","title":"type doModule","text":"
    type doModule<S extends State, T extends ThunkModule<S>> = {\n  // @ts-expect-error toThunkFuncMap includes only ThunkFunc<S> | BaseActionFunc\n  [action in keyof toThunkFuncMap<T>]: VoidReturnType<toThunkFuncMap<T>[action]>\n} & Omit<defaultDoModule, keyof toThunkFuncMap<T>>\n

    Functions in doModule already wrap thunk functions with set (dispatch in Redux / useReducer). doModule functions can be directly used. We don't wrap doModule functions with set/dispatch.

    "},{"location":"04-apis/#type-modulestate","title":"type ModuleState","text":"
    type ModuleState<S extends State> = {\n  name: string\n  nodes: NodeStateMap<S>\n  defaultState: S\n  defaultID?: string | null\n}\n
    "},{"location":"04-apis/#type-customgenid","title":"type CustomGenID","text":"
    type CustomGenID = () => string\n

    Module state.

    "},{"location":"04-apis/#primitive-thunk-functions_1","title":"Primitive Thunk Functions","text":""},{"location":"04-apis/#setdefaultidid-string","title":"setDefaultID(id: string)","text":"
    const setDefaultID = (id): BaseAction\n

    Set default id in module state.

    "},{"location":"04-apis/#module-state-related","title":"Module State Related","text":""},{"location":"04-apis/#getstatebymodulemodulestate-id","title":"getStateByModule(moduleState, id?)","text":"
    const getStateByModule = <S extends State>(\n  moduleState: ModuleState<S>,\n  id?: string | null,\n): Readonly<S>\n

    [Guaranteed] Get the state from module state. id as ensured defaultID if id is not present. Create a state with defaultState in moduleState if state does not exist.

    [NOTICE] Used only within thunks or event handles and effect hooks in components. Use useThunk in component rendering.

    "},{"location":"04-apis/#getstateornullbymodulemodulestate-id","title":"getStateOrNullByModule(moduleState, id?)","text":"
    const getStateOrNullByModule = <S extends State>(\n  moduleState: ModuleState<S>,\n  id?: string | null,\n): Readonly<S | null>\n

    Get the state from module state. Return null if the state does not exist.

    "},{"location":"04-apis/#getnodeornullbymodulemodulestate-id","title":"getNodeOrNullByModule(moduleState, id?)","text":"
    const getNodeOrNullByModule = <S extends State>(\n  moduleState: ModuleState<S>,\n  id?: string | null,\n): Readonly<NodeState<S> | null>\n

    Get the node from module state. Return null if the node does not exist.

    "},{"location":"04-apis/#getdefaultidmodulestate","title":"getDefaultID(modulestate)","text":"
    const getDefaultID = <S extends State>(moduleState: ModuleState<S>): string | null | undefined\n

    Get defaultID.

    "}]} \ No newline at end of file