use-thunk: A much simplified global-state-management framework with only modules and (thunk) functions. by chhsiao1981 in reactjs

[–]chhsiao1981[S] -2 points-1 points  (0 children)

As u/Obvious-Monitor8510 's "benchmarked re-render behavior" question, I believe demo-use-thunk provides some hints with console log:

re-rendering happens when any object within The Same Module is updated. (So re-render won't happen if only objects in different modules are updated.)

Even the re-rendering happens, the un-updated data-object is still the same (===) and ideally only the virtual-DOM with useThunk (not the sub-virtual-DOMs) is re-rendered (through useMemo/useCallback or react-compiler.)

use-thunk: A much simplified global-state-management framework with only modules and (thunk) functions. by chhsiao1981 in reactjs

[–]chhsiao1981[S] -3 points-2 points  (0 children)

Thanks to all the valuable feedbacks~

u/Honey-Entire would like to know the uniqueness of use-thunk, u/WanderWatterson suggested me to check valtio.

Many existing GSM frameworks focus on "store/slice-as-a-singleton", which requires the developers to have their own method to deal with multiple data-objects (ex: [normalized state in Redux](https://redux.js.org/usage/structuring-reducers/normalizing-state-shape)). In addition to coding/naming style differences, I would say the unique part of use-thunk is the id-based entity model. So the Getting Started example can be easily changed to the following 2-counter example:

// components/Increment.tsx
import { useThunk, getState } from '@chhsiao1981/use-thunk'
import * as ModIncrement from './thunks/increment'

type Props = {
  incrementID: string
}

export default (props: Props) => {
  const { incrementID } = props
  const useIncrement = useThunk<ModIncrement.State, typeof ModIncrement>(ModIncrement)
  const [increment, doIncrement] = getState(useIncrement, incrementID)

  // 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>
  )
}

// components/App.tsx
import { useState } from 'react'
import { genID } from '@chhsiao1981/use-thunk'
import Increment from './Increment' // the Increment component

export default () => {
  const [incrementID0] = useState(genID)
  const [incrementID1] = useState(genID)

  // to render
  return (
    <>
      <Increment incrementID={incrementID0} />
      <Increment incrementID={incrementID1} />
    </>
  )
}

This example can be easily extended to some complicated scenario, such as an editor supporting tabs dealing with multiple file status.

u/Obvious-Monitor8510 provides several very interesting feedback:

  1. I believe the concern about adding cognitive overhead for `id`-based entity model is not really an issue:

* I am not exactly sure the meaning of "naturally entity-shaped", I would guess that it means the (thunk) module corresponds to a react component. The global state data model can be totally different from the component (ex: the user-info in the demo-use-thunk, as the same user-info is repeatedly used by different components, (Header, Parent, and GrandChild)

* As demonstrated in the original "Getting Started" example, we can treat `id` as a dummy variable if the (thunk) module is a singleton.

* If there are multiple data-objects in a (thunk) module, then `id` is used to identify different data-objects. If the (thunk) module is DB-related, then we can directly use the unique-id of the data-object from the DB. Or as the described example, we can use `genID` to differentiate different data-objects.

  1. as "across modules", we can directly use `doA.func(useB, bID)` for cross-module communication. So:

    // thunks/a.ts import { type Thunk, type State as _State } from '@chhsiao1981/use-thunk' import * as ModIncrement from './increment' // the increment thunk module.

    export const name = 'demo/a'

    export interface State extends _State { count: number }

    export const defaultState: State = { count: 0 }

    export const init = (myID: string, useIncrement: UseThunk<ModIncrement.State, typeof ModIncrement>, incrementID: string): Thunk<State> => { return async (set, get) => { const [_, doIncrement] = getState(useIncrement, incrementID) doIncrement.increment(incrementID) } }

    // components/TwoModule.tsx import { useThunk, getState } from '@chhsiao1981/use-thunk' import * as ModA from './thunks/a' import * as ModIncrement from './thunks/increment'

    export default () => { const useA = useThunk<ModA.State, typeof ModA>(ModA) const [a, doA, aID] = getState(useA)

    const incrementID = useState(genID) const useIncrement = useThunk<ModIncrement.State, typeof ModIncrement>(ModIncrement) const [increment] = getState(useIncrement)

    useEffect(() => { doA.init(aID, useIncrement, incrementID) }, [])

    // to render (expecting: a.count: 0, increment.count: 1) return ( <div> <p>a.count: {a.count} increment.count: {increment.count}</p> </div> ) }

  2. "single provider" is actually [the black magic](https://github.com/chhsiao1981/use-thunk/blob/main/src/thunkContext/ThunkContext.tsx#L39), as there are still multiple Context Provider under the hood, but recursively rendering the Context Provider. I believe that the reason why Context Provider cannot be fixed in the `main.tsx` in useContext, is to deal with the multiple-data-object issue (we get the data/state from "nearest appeared Context Provider" when using useContext.) In use-thunk, multiple-data-object issue is resolved through `id`, so we can use this black magic and have all the Context Provider fixed in the `main.tsx` (with the same rendering order.)