Created
July 20, 2023 19:41
-
-
Save devdave/ebe8910739e36708852c1655179d2fbd to your computer and use it in GitHub Desktop.
Experimental React settings state manager
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { map, Dictionary } from 'lodash' | |
import { UseMutationResult, UseQueryResult } from '@tanstack/react-query' | |
import { useCallback } from 'react' | |
type GenericTypes<TContainer> = TContainer[keyof TContainer] | |
export interface SettingsManagerReturn<TValues> { | |
get: <Field extends keyof TValues>(name: Field) => UseQueryResult<TValues[Field]> | undefined | |
set: <Field extends keyof TValues>(name: Field, value: TValues[Field]) => TValues[Field] | |
reconcile: (onValuesLoaded?: (values: TValues) => void) => void | |
makeState: <Field extends keyof TValues>( | |
name: Field | |
) => [TValues[Field] | undefined, boolean, (value: TValues[Field]) => void] | |
} | |
export function useSettings<TValues extends object = Record<string, unknown>>({ | |
defaultSettings, | |
setter, | |
getter, | |
bulkFetchSettings, | |
bulkDefaultSetter | |
}: { | |
defaultSettings: TValues | |
bulkFetchSettings: () => Promise<TValues> | |
setter: UseMutationResult<undefined, unknown, { name: string; value: string }, unknown> | |
getter: (<Field extends keyof TValues>(name: Field) => UseQueryResult<TValues[Field]>) | undefined | |
bulkDefaultSetter: (changeset: object[]) => Promise<object> | |
}): SettingsManagerReturn<TValues> { | |
/** | |
* Internalized getter | |
* | |
* Eventually a place to use a cached copy versus calling home constantly | |
*/ | |
const get = useCallback( | |
<Field extends keyof TValues>(name: Field): UseQueryResult<TValues[Field]> | undefined => | |
getter ? getter(name) : undefined, | |
[getter] | |
) | |
/** | |
* Set the value of a Setting | |
* | |
* @param name | |
* @param value | |
*/ | |
const set = useCallback( | |
<Field extends keyof TValues>(name: Field, value: TValues[Field]) => { | |
console.log('Would set', name, value) | |
if (setter) { | |
const coerce_name = name as string | |
const coerce_value = value as string | |
setter.mutate({ name: coerce_name, value: coerce_value }) | |
} | |
// eslint-disable-next-line no-param-reassign | |
return value | |
}, | |
[setter] | |
) | |
/** | |
* | |
* Given a property name of TValue, return a useState like array | |
* | |
* @param name | |
* @return [value, isLoading, updater] | |
*/ | |
const makeState = useCallback( | |
<Field extends keyof TValues>( | |
name: Field | |
): [TValues[Field] | undefined, boolean, (new_val: TValues[Field]) => void] => { | |
const value = get(name) | |
if (value === undefined) { | |
throw new Error('Missing getter for makeState') | |
} | |
return [value.data, value.isLoading, (new_val: TValues[Field]) => set(name, new_val)] | |
}, | |
[get, set] | |
) | |
/** | |
* Must be called before any other member function is used | |
* | |
*/ | |
const reconcile = useCallback( | |
(onValuesLoaded: undefined | ((changeset: TValues) => void) = undefined) => { | |
console.log('Ensuring defaults are set') | |
const changeset = map( | |
defaultSettings as Dictionary<unknown>, | |
( | |
name: keyof TValues, | |
value: GenericTypes<TValues> | |
): { name: keyof TValues; value: GenericTypes<TValues>; type: string } => ({ | |
name, | |
value, | |
type: typeof defaultSettings[name] as string | |
}) | |
) | |
console.log(`Defaults are ${JSON.stringify(changeset)}`) | |
const coerced = changeset as unknown | |
bulkDefaultSetter(coerced as Array<object>) | |
.then(() => console.log('Defaults set')) | |
.catch((reason) => console.error('Defaults failed with', reason)) | |
if (onValuesLoaded) { | |
bulkFetchSettings().then((payload: TValues) => { | |
onValuesLoaded(payload) | |
}) | |
} | |
}, | |
[bulkDefaultSetter, defaultSettings] | |
) | |
return { get, set, reconcile, makeState } | |
} | |
/** | |
* Partial example usage | |
* | |
*/ | |
/** | |
// Requires a type interface | |
interface TestSettings { | |
delay: number | |
disableSomethingImportant: boolean | |
thingName: string | |
} | |
const settings = useSettings<TestSettings>({ | |
getter: (settingName)=>api.fetchSetting(settingName), | |
setter: (settingName, settingValue)=>api.setSetting(settingName settingValue), | |
defaultSetter: undefined, | |
defaultSettings: { | |
delay: 123, | |
disableSomethingImportant: false, | |
thingName: 'igor' | |
} | |
}) | |
const = onValuesFetched(values:TestSettings) => { | |
forEach(values, (name, value)=>{ | |
// For cases like react query when you don't want to spam the hell | |
// out of the backend | |
localApi.addToSettingsCache(name, value) | |
}) | |
//For a scenario where the application is waiting to proceed | |
setAppSettingAreReady(true) | |
} | |
// call when api bridge is up | |
settings.reconcile() | |
const [thingName, thingIsLoading, setThingName] = settings.makeState('thingName') | |
const [delay, delayIsLoading, setDelay] = settings.makeState('delay') | |
const [toggle1, toggleIsLoading, setToggle1] = settings.makeState('disableSomethingImportant') | |
if (thingIsLoading || delayIsLoading || toggleIsLoading) { | |
console.log('Stuff is still loading!') | |
} | |
console.log('thing is ', thingName) | |
console.log('delay is', delay) | |
console.log('toggle1 is', toggle1) | |
setThingName('Bob') | |
setDelay(856) | |
setToggle1(!toggle1) | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment