Skip to content

Commit

Permalink
feat!: improve substate object format
Browse files Browse the repository at this point in the history
- switch useSubstate from returning array to returning object
- improve type checking
- refactor some awkward types
- remove useGlobalDispatch
- rework useDispatch to do what useGlobalDispatch used to do
- refer to GlobalDispatch now as GenericDispatch

BREAKING CHANGE:

This commit alters the function and type API in a backwards-incompatible way in preparation for v6.
  • Loading branch information
Harvtronix committed Nov 13, 2024
1 parent d340069 commit 923a2c9
Show file tree
Hide file tree
Showing 12 changed files with 74 additions and 90 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
}
}
],
"jsdoc/require-param-type": "off",
"jsdoc/require-returns-type": "off",
"max-len": ["warn", 100],
"no-undef": "off",
"no-unused-vars": "off",
Expand Down
8 changes: 4 additions & 4 deletions packages/examples-18/src/components/BasicExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ const actions = {
)
}

// Use it like you would `useReducer` or `useState`
// Use it (mostly) like you would `useReducer` or `useState`
const BasicExample = () => {
const [test, dispatch] = useSubstate(substates.test)
const test = useSubstate(substates.test)

return (
<button
onClick={() => (dispatch(actions.updateSomeField, 'the new state'))}
onClick={() => (test.dispatch(actions.updateSomeField, 'wow-some-stuff'))}
>
{test.someField}
{test.current.someField}
</button>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface Test {
someField: string
}

// Set up a substate
// Set up a substate by passing a state generator function to seed the Substate
const substates = {
test: createSubstate<Test>(() => ({someField: 'it is generated!'})),
}
Expand All @@ -22,15 +22,15 @@ const actions = {
)
}

// Use it like you would `useReducer` or `useState`
// Use the hook to get the current state data and dispatch actions
const BasicExampleWithGenerator = () => {
const [test, dispatch] = useSubstate(substates.test)
const test = useSubstate(substates.test)

return (
<button
onClick={() => (dispatch(actions.updateSomeField, 'not anymore!'))}
onClick={() => (test.dispatch(actions.updateSomeField, 'not anymore!'))}
>
{test.someField}
{test.current.someField}
</button>
)
}
Expand Down
6 changes: 3 additions & 3 deletions packages/examples-18/src/components/DispatchOnlyExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@ const actions = {
)
}

// Use it like you would `useReducer` or `useState`
// Use it similarly to how you would `useReducer` or `useState`
const DispatchOnlyExample = () => {
const dispatch = useDispatch(substates.test)
const dispatch = useDispatch()

useEffect(() => {
console.log('I will only render this one time!') // This will only ever be called one time
})

return (
<button
onClick={() => (dispatch(actions.updateButtonText, 'the new state'))}
onClick={() => (dispatch(substates.test, actions.updateButtonText, 'the new state'))}
>
the button
</button>
Expand Down
10 changes: 5 additions & 5 deletions packages/examples-18/src/components/PatchEffectExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ const actions = {
}

const PatchEffectExample = () => {
const [test, testDispatch] = useSubstate(substates.test)
const anotherTestDispatch = useDispatch(substates.anotherTest)
const test = useSubstate(substates.test)
const dispatch = useDispatch()

// Create a patch effect for a single substate
usePatchEffect((patches) => {
Expand All @@ -55,15 +55,15 @@ const PatchEffectExample = () => {
<>
<button
onClick={() => (
testDispatch(actions.test.updateButtonText, 'the new state')
test.dispatch(actions.test.updateButtonText, 'the new state')
)}
>
{test.field1}
{test.current.field1}
</button>

<button
onClick={() => (
anotherTestDispatch(actions.anotherTest.updateOtherButtonText, 'baz')
dispatch(substates.anotherTest, actions.anotherTest.updateOtherButtonText, 'baz')
)}
>
Dispatch action to update "anotherTest"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ const actions = {
}

const ReplaceEntireStateExample = () => {
const [test, dispatch] = useSubstate(substates.test)
const test = useSubstate(substates.test)

return (
<button
onClick={() => (dispatch(actions.updateButtonText, 'after'))}
onClick={() => (test.dispatch(actions.updateButtonText, 'after'))}
>
{test}
{test.current}
</button>
)
}
Expand Down
35 changes: 15 additions & 20 deletions packages/react-substate/src/Interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
interface ActionStateModifier<Draft = any, Payload = any> {
(draft: Draft, payload: Payload): Draft | void
}
type ActionStateModifier<Draft = any, Payload = any> =
(draft: Draft, payload: Payload) => Draft | void

interface Actions {
[key: string]: ActionStateModifier
Expand All @@ -11,9 +10,7 @@ interface ActionKey<Payload> {
__payload: () => Payload
}

interface PatchEffectFunction {
(patches: Array<any>): void
}
type PatchEffectFunction = (patches: Array<any>) => void

interface Substates {
[key: string]: {
Expand All @@ -28,20 +25,18 @@ interface SubstateKey<Type> {
__type: () => Type
}

interface Dispatcher {
<Payload>(
actionKey: ActionKey<Payload>,
payload: ReturnType<typeof actionKey['__payload']>
): void
}
type Dispatcher =
<Key extends ActionKey<unknown>>(
actionKey: Key,
payload: ReturnType<Key['__payload']>
) => void

interface GlobalDispatcher{
<Payload>(
substateKey: SubstateKey<unknown>,
actionKey: ActionKey<Payload>,
payload: ReturnType<typeof actionKey['__payload']>
): void
}
type GenericDispatcher =
<SKey extends SubstateKey<unknown>, AKey extends ActionKey<unknown>>(
substateKey: SKey,
actionKey: AKey,
payload: ReturnType<AKey['__payload']>
) => void

interface DevToolsState {
[key: string]: {
Expand Down Expand Up @@ -71,7 +66,7 @@ export type {
DevToolsOperation,
DevToolsState,
Dispatcher,
GlobalDispatcher,
GenericDispatcher,
PatchEffectFunction,
Substates,
SubstateKey
Expand Down
21 changes: 13 additions & 8 deletions packages/react-substate/src/hooks/useDispatch.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import {useCallback} from 'react'

import {dispatch} from '../dispatch.js'
import {ActionKey, Dispatcher, SubstateKey} from '../Interfaces.js'
import {ActionKey, GenericDispatcher, SubstateKey} from '../Interfaces.js'

/**
* Hook that allows a component to receive a reference to a dispatch function that can be called to
* update a particular substate without also listening for changes to any substates.
* update ANY substate without also listening for changes to a substate.
*
* @param {SubstateKey<*>} substateKey The substate to be modified by actions dispatched via the
* returned dispatch function.
* @returns {Dispatcher} Dispatch function that can be called to update the substate.
* Similar to the dispatch function obtained via `useSubstate`, except this function requires a
* reference to a Substate as the first argument.
*
* @returns Dispatch function that can be called to update any substate.
*/
export function useDispatch <Type> (substateKey: SubstateKey<Type>): Dispatcher {
export function useDispatch (): GenericDispatcher {
// Since we are creating a function in this hook, memoize it so it remains the same across
// re-renders
return useCallback(
<Payload>(actionKey: ActionKey<Payload>, payload: Payload) => (
<Payload>(
substateKey: SubstateKey<unknown>,
actionKey: ActionKey<Payload>,
payload: Payload
) => (
dispatch(substateKey, actionKey, payload)
),
[substateKey]
[]
)
}
25 changes: 0 additions & 25 deletions packages/react-substate/src/hooks/useGlobalDispatch.ts

This file was deleted.

33 changes: 21 additions & 12 deletions packages/react-substate/src/hooks/useSubstate.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
import {useEffect, useState} from 'react'
import {useCallback, useEffect, useState} from 'react'

import {log} from '../Debug.js'
import {Dispatcher, SubstateKey} from '../Interfaces.js'
import {dispatch} from '../dispatch.js'
import {ActionKey, Dispatcher, SubstateKey} from '../Interfaces.js'
import {
getSubstate,
hasSubstate,
registerListener,
unregisterListener
} from '../managers/SubstateManager.js'
import {useDispatch} from './useDispatch.js'

/**
* Hook that allows a component to listen for changes to a substate and receive a reference to a
* dispatch function that can be called to update that substate.
*
* @param {SubstateKey<*>} substateKey The key of the substate to return. The returned dispatch
* @param substateKey The key of the substate to return. The returned dispatch
* function will be scoped to this substate as well.
* @returns {Array} Array whose `0` index is the current value of the substate and whose `1` index
* is a dispatch function that can be called to update the substate.
* @returns Object containing the current value of the Substate and a dispatch function that can be
* called to update it.
*/
export function useSubstate <Type> (substateKey: SubstateKey<Type>): [Type, Dispatcher] {
export function useSubstate <Type> (
substateKey: SubstateKey<Type>
): {current: Type, dispatch: Dispatcher} {
if (!hasSubstate(substateKey)) {
throw new Error('No substate found with key ' + substateKey)
}

const dispatch = useDispatch(substateKey)
// Since we are creating a function in this hook, memoize it so it remains the same across
// re-renders
const substateDispatch: Dispatcher = useCallback(
<Payload>(actionKey: ActionKey<Payload>, payload: Payload) => (
dispatch(substateKey, actionKey, payload)
),
[substateKey]
)

const [, setState] = useState()

Expand All @@ -38,8 +47,8 @@ export function useSubstate <Type> (substateKey: SubstateKey<Type>): [Type, Disp
}
}, [substateKey, setState])

return [
getSubstate(substateKey),
dispatch
]
return {
current: getSubstate(substateKey),
dispatch: substateDispatch
}
}
6 changes: 2 additions & 4 deletions packages/react-substate/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {setDebugEnabled} from './Debug.js'
import {useDispatch} from './hooks/useDispatch.js'
import {useGlobalDispatch} from './hooks/useGlobalDispatch.js'
import {usePatchEffect} from './hooks/usePatchEffect.js'
import {useSubstate} from './hooks/useSubstate.js'
import {Dispatcher, GlobalDispatcher} from './Interfaces.js'
import {Dispatcher, GenericDispatcher} from './Interfaces.js'
import {createAction} from './managers/ActionManager.js'
import {setDevToolsEnabled} from './managers/DevToolsManager.js'
import {createSubstate} from './managers/SubstateManager.js'
Expand All @@ -26,12 +25,11 @@ export {
setDevToolsEnabled,

useDispatch,
useGlobalDispatch,
usePatchEffect,
useSubstate
}

export type {
Dispatcher,
GlobalDispatcher
GenericDispatcher
}
2 changes: 1 addition & 1 deletion packages/react-substate/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist/mjs", /* Specify an output folder for all emitted files. */
"removeComments": true, /* Disable emitting comments. */
"removeComments": false, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
Expand Down

0 comments on commit 923a2c9

Please sign in to comment.