From 229993442dc250d7f787256a9760176901962678 Mon Sep 17 00:00:00 2001 From: Keiichiro Amemiya Date: Mon, 1 Jan 2024 17:56:52 +0900 Subject: [PATCH] feat: allow function as setReplicant value --- src/use-replicant.ts | 67 +++++++++++++++++------------------- tests/use-replicant.spec.tsx | 4 +-- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/src/use-replicant.ts b/src/use-replicant.ts index a054fdf..16398b6 100644 --- a/src/use-replicant.ts +++ b/src/use-replicant.ts @@ -1,6 +1,15 @@ -import {useEffect, useState} from 'react'; +import {useEffect, useMemo, useState} from 'react'; import {klona as clone} from 'klona/json'; -import type NodeCG from '@nodecg/types'; + +type JsonValue = boolean | number | string | null; + +type Json = JsonValue | JsonValue[] | {[key: string]: Json}; + +export type UseReplicantOptions = { + defaultValue?: T; + bundle?: string; + persistent?: boolean; +}; /** * Subscribe to a replicant, returns tuple of the replicant value and `setValue` function. @@ -10,44 +19,28 @@ import type NodeCG from '@nodecg/types'; * @param initialValue Initial value to pass to `useState` function * @param options Options object. Currently supports the optional `namespace` option */ -export const useReplicant = ( +export const useReplicant = ( replicantName: string, - initialValue: T, - options?: NodeCG.Replicant.Options & {namespace?: string}, -): [T | undefined, (newValue: T) => void] => { - const [value, updateValue] = useState(initialValue); - - const replicantOptions = - options && - ({ - persistent: options.persistent, - schemaPath: options.schemaPath, - } satisfies typeof options); - - if (options && 'defaultValue' in options) { - ( - replicantOptions as NodeCG.Replicant.OptionsWithDefault - ).defaultValue = options.defaultValue; - } + {bundle, defaultValue, persistent}: UseReplicantOptions = {}, +) => { + const replicant = useMemo(() => { + if (typeof bundle === 'string') { + return nodecg.Replicant(replicantName, bundle, { + defaultValue, + persistent, + }); + } + return nodecg.Replicant(replicantName, {defaultValue, persistent}); + }, [bundle, defaultValue, persistent, replicantName]); - let replicant: NodeCG.ClientReplicant; - if (options?.namespace) { - replicant = nodecg.Replicant( - replicantName, - options.namespace, - replicantOptions, - ); - } else { - replicant = nodecg.Replicant(replicantName, replicantOptions); - } + const [value, setValue] = useState(replicant.value); useEffect(() => { const changeHandler = (newValue: T | undefined): void => { - updateValue((oldValue) => { + setValue((oldValue) => { if (newValue !== oldValue) { return newValue; } - // replicant.value has always the same reference. Cloning to cause re-rendering return clone(newValue); }); }; @@ -59,8 +52,12 @@ export const useReplicant = ( return [ value, - (newValue) => { - replicant.value = newValue; + (newValue: T | ((oldValue?: T) => void)) => { + if (typeof newValue === 'function') { + newValue(replicant.value); + } else { + replicant.value = newValue; + } }, - ]; + ] as const; }; diff --git a/tests/use-replicant.spec.tsx b/tests/use-replicant.spec.tsx index 5035c0f..2e154db 100644 --- a/tests/use-replicant.spec.tsx +++ b/tests/use-replicant.spec.tsx @@ -75,7 +75,7 @@ interface RunnerNameProps { const RunnerName: React.FC = (props) => { const {prefix} = props; const repName = `${prefix ?? 'default'}:currentRun`; - const [currentRun] = useReplicant(repName, null, { + const [currentRun] = useReplicant(repName, { defaultValue: {runner: {name: 'foo'}}, }); if (!currentRun) { @@ -86,7 +86,7 @@ const RunnerName: React.FC = (props) => { // Example of a replicant with a mutating value. const Counter: React.FC = () => { - const [counter, setCounter] = useReplicant('counter', 0, { + const [counter, setCounter] = useReplicant('counter', { defaultValue: 0, }); if (typeof counter !== 'number') return null;