Introduction
Global State Management (GSM) in ReactJS
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.
Caveats of React Redux (and Redux Toolkit (RTK))
However, there are several caveats of React Redux / RTK:
- We need to know
configureStore,createReducer,createSlice, and many other definitions before good usage of React Redux / RTK. - Need to know relationship between action and reducer.
- Most of the definitions are in the
createSlicefunctions. These functions can be complex gigantic functions in a complex app. - Not really about "objects-of-the-same-kind" and requires developers have their own method.
Reduxrecommends normalized states. - Several redundant code when programming.
Caveats of zustand
Recently zustand significantly simplify the use of Redux / RTK. However:
- Still requires developers have their own methods for "objects-of-the-same-kind".
- We still need to know the relationship between store vs. slice (
useBoundStore). - I feel that the
createBearFishSliceexample is actually awkward. Why do we need to create additional slices if we want to update the states from 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."
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.
Goals of use-thunk
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.
- 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.
-
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.
-
-
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’s state indirectly by invoking its exposed public functions.
-
Component Interface Simplicity: Components do not require a
dispatchreference. They interact with state by directly invoking clean, module-scoped functions, minimizing inline data manipulation.
-
-
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
idparameters, operations are strictly scoped to a target entity, eliminating accidental collateral state updates. -
Singleton Fallback: The
idparameter in a module is entirely optional. When omitted, the module gracefully falls back with a uniquely identified default id (different for different modules).
-
Implementation
To achieve the goals:
- Heavily use the concept of
Thunk, to be able to have multiple computations/reductions in one operation. - With the concept of "normalized state" in mind:
State: the state of each object.NodeState: the metadata of each object, including theidandState.ModuleState: the collection ofNodeStatein a module.
- The thunk functions are automatically attached with
dispatchwhen used by the components. There is no need to usedispatchin components. - API mainly exposes accessing module-based thunk functions, but reading only object-state-based data.
- Primitive thunk functions (init, upsert, update, remove) are implemented interally. The developers just call these primitive thunk functions to update the object-state.
- Use
useSyncExternalStoreto achieve object-state based re-rendering. - Rename
dispatch/getStatetoset/get/getOrNull/dispatch/getModuleStatefor extended and easier to use.
Primitive Thunk Functions
Primitive thunk functions are similar to the original actions in Redux.
We provide the following default primitive thunk functions:
init: initialize an object-state.upsert: update the object-state, with initialization first if not exist.update: update the object-state.remove: remove the object-state.