A framework for easily managing global data state with useThunk, with zustand-like taste.
Inspired by the concepts of Redux Thunk and Redux Duck.
src/useThunkReducer.ts is adapted from nathanbuchar/react-hook-thunk-reducer.
For more information, please check docs/00-introduction.md.
For usage examples, please refer to demo-use-thunk.
npm install --save @chhsiao1981/use-thunk
Thunk module able to do increment (thunks/increment.ts):
import { type Thunk, type State as rState, update } from '@chhsiao1981/use-thunk'
export const name = 'demo/Increment'
export interface State extends rState {
count: number
}
export const defaultState: State = {
count: 0
}
// upsert directly with set.
export const increment = (myID: string, num: number = 1): Thunk<State> => {
return async (set, get) => {
let me = get(myID)
const {count} = me
set(myID, { count: count + num })
}
}
// or we can treat set as dispatching a base action.
export const increment2 = (myID: string): Thunk<State> => {
return async (set, get) => {
let me = get(myID)
const {count} = me
set(update({ count: count + 2 }))
}
}
// or we can use set as dispatching a thunk function.
export const increment3 = (myID: string): Thunk<State> => {
return async (set) => {
set(increment(myID, 3))
}
}components/App.tsx:
import { type ThunkModuleToFunc, useThunk, getState } from '@chhsiao1981/use-thunk'
import * as DoIncrement from './thunks/increment'
type doIncrement = toDoModule(typeof DoIncrement)
export default () => {
const useIncrement = useThunk<DoIncrement.State, doIncrement>(DoIncrement)
const [increment, doIncrement, incrementID] = getState(useIncrement)
// to render
return (
<div>
<p>count: {increment.count}</p>
<button onClick={() => doIncrement.increment(incrementID)}>increase 1</button>
<button onClick={() => doIncrement.increment2(incrementID)}>increase 2</button>
<button onClick={() => doIncrement.increment3(incrementID)}>increase 3</button>
</div>
)
}main.tsx:
import { registerThunk, ThunkContext } from "@chhsiao1981/use-thunk";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import * as DoIncrement from './thunks/increment'
import App from "./components/App";
registerThunk(DoIncrement)
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ThunkContext>
<App />
</ThunkContext>
</StrictMode>,
)import type { State as rState } from '@chhsiao1981/use-thunk'
// Thunk-module name.
export const name = ""
// state definition of the reducer.
export interface State extends rState {
}
export const defaultState: State = {}
.
.
.import { type toDoModule, useThunk, getState } from '@chhsiao1981/use-thunk'
import * as DoModule from '../thunks/module'
type doModule = toDoModule<typeof DoModule>
const Component = () => {
const useModule = useThunk<DoModule.State, doModule>(DoModule)
const [module, doModule, moduleID] = getState(useModule)
.
.
.
}import { registerThunk, ThunkContext } from '@chhsiao1981/use-thunk'
import * as DoModule from '../thunks/module'
registerThunk(DoModule)
.
.
.
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ThunkContext>
<App />
</ThunkContext>
</StrictMode>,
)The general concept of normalized state can be found in Normalizing State Shape with the following features:
- ClassState: the state of the class, including the nodes and the defaultID of the class.
- NodeState: the state of a node, including the id of the node and the content (state) of the node.
- State: the content of the node, represented as a state.
For example, the example in the redux link is represented as:
moduleStatePost = {
name: 'post',
nodes: {
[uuid-post1] : {
id: uuid-post1,
state: {
author : uuid-user1,
body : "......",
comments: [uuid-comment1, uuid-comment2]
},
},
[uuid-post2] : {
id : uuid-post2,
state: {
author : uuid-user2,
body : "......",
comments: [uuid-comment3, uuid-comment4, uuid-comment5]
}
}
},
defaultID,s
defaultState,
}and:
moduleStateComment = {
myClass: 'module',
nodes: {
[uuid-comment1] : {
id: uuid-comment1,
state: {
author : uuid-user2,
comment : ".....",
}
},
[uuid-comment2] : {
id : uuid-comment2,
state: {
author : uuid-user3,
comment : ".....",
}
},
[uuid-comment3] : {
id : uuid-comment3,
state: {
author : uuid-user3,
comment : ".....",
}
},
[uuid-comment4] : {
id : uuid-comment4,
state: {
author : uuid-user1,
comment : ".....",
}
},
[uuid-comment5] : {
id : uuid-comment5,
state: {
author : uuid-user3,
comment : ".....",
}
}
}
defaultID,
defaultState,
}and:
moduleStateUser = {
name: 'user',
nodes: {
[uuid-user1] : {
id: uuid-user1,
state: {
username : "user1",
name : "User 1",
}
},
[uuid-user2] : {
id: uuid-user2,
state: {
username : "user2",
name : "User 2",
}
},
[uuid-user3] : {
id: uuid-user3,
state: {
username : "user3",
name : "User 3",
}
}
}
defaultID,
defaultState,
}Thunk is defined as:
export type Thunk<S extends State> = (
set: set<S>,
get: (id?: string) => S,
getOrNull: (id?: string) => S | null | undefined,
dispatch: dispatch<S>,
getModuleState: () => ModuleState<S>,
) => voidWe generally use only set and get.
export type ThunkModule<S extends State> = {
name: string
defaultState: S
// The rest of the variables are doModule.
// Specifying index-signatures to include all the variables.
[action: string]: ThunkFunc<S> | string | S
}export interface doModule<S extends State> {
[action: string]: ThunkFunc<S>
}export type ThunkFunc<S extends State> = (...params: any[]) => Thunk<S>register a thunk module.
Rendering thunk context.
Similar to React.useReducer, but we use useThunk, and we also bind the actions with set (similar concept as mapDispatchToProps).
return: UseThunk
Get the state of myID by UseThunk. Get the state of defaultID if myID is not present. Return defaultState if not available.
return: [state, doModule, theID]
o
export type UseThunk<S extends State, R extends doModule<S>> = [Readonly<ModuleState<S>>, setMap<S, R>]export type setMap<S extends State, T extends doModule<S>> = {
[action in keyof T]: VoidReturnType<T[action]>
} & Omit<DefaultSetMap, keyof T>initialize the state.
update the data to myID.
initialize the state if it does not exist, and update the data to myID.
remove the state.
Get the state of myID. Get the state of defaultID if myID is not present. Return null if not available.
Get the state of myID. Get the state of defaultID if myID is not present. Return defaultState if not available.
generate id for the state.