Skip to content

Commit 3ad8ffb

Browse files
Introduce createSharedState, createSharedFunction, and createSharedSubscription.
- Added factory functions (`createSharedState`, `createSharedFunction`, `createSharedSubscription`) for initializing shared resources with `key` generation and scoped prefixes. - Updated `useSharedState`, `useSharedFunction`, and `useSharedSubscription` to accept created objects as arguments, providing streamlined usage. - Introduced generic `SharedCreated` type for unified handling of shared resources. - Refactored utility functions to include a `random` key generator for consistent key creation. - Updated SharedData methods (`clear` and `resolve`) to accept the new shared objects. - Adjusted documentation exports to include the new types and functions. - Improved demo examples to showcase the new features.
1 parent c336424 commit 3ad8ffb

File tree

9 files changed

+220
-71
lines changed

9 files changed

+220
-71
lines changed

demo/app.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import './FakeSharedEmitter';
1111
import {FakeSharedEmitter} from "./FakeSharedEmitter";
1212
import {useEffect, useState} from "react";
13+
import {createSharedState} from "../src/hooks/use-shared-state";
1314

1415
FakeSharedEmitter.intervalDuration = 3000;
1516
window.sharedSubscriptionsApi = sharedSubscriptionsApi;
@@ -18,8 +19,12 @@ window.sharedStatesApi = sharedStatesApi;
1819

1920
sharedStatesApi.set("x", 55);
2021

22+
const counterGlobal = createSharedState(0);
23+
2124
const Comp1 = () => {
22-
const [x, setX] = useSharedState('x', 0);
25+
//const [x, setX] = useSharedState('x', 0);
26+
//const [x, setX] = useSharedState(counterGlobal);
27+
const [x, setX] = useSharedState("counterGlobal", "");
2328
const handle = (by = 1) => {
2429
setX(x+by)
2530
}
@@ -77,7 +82,8 @@ const Comp2 = () => {
7782
const App = () => {
7883

7984
const [hide, setHide] = useState(false);
80-
const [x, setX] = useSharedState('x', 0);
85+
const [x, setX] = useSharedState(counterGlobal);
86+
//const [x, setX] = useSharedState('x', 0);
8187
//const [x, setX] = useState(0);
8288
const handle = () => {
8389
setX(x+1)

src/SharedData.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {AFunction, DataMapValue, Prefix} from "./types";
1+
import type {AFunction, DataMapValue, Prefix, SharedCreated} from "./types";
22
import {useEffect} from "react";
33
import {ensureNonEmptyString, log} from "./lib/utils";
44

@@ -149,14 +149,34 @@ export class SharedApi<T>{
149149
});
150150
}
151151

152+
/**
153+
* resolve a shared created object to a value
154+
* @param sharedCreated
155+
*/
156+
resolve(sharedCreated: SharedCreated) {
157+
const {key, prefix} = sharedCreated;
158+
return this.get(key, prefix);
159+
}
160+
161+
clear(key: string, scopeName: Prefix): void;
162+
clear(sharedCreated: SharedCreated): void;
152163
/**
153164
* clear a value from the shared data
154165
* @param key
155166
* @param scopeName
156167
*/
157-
clear(key: string, scopeName: Prefix) {
158-
const prefix: Prefix = scopeName || "_global";
159-
this.sharedData.clear(key, prefix);
168+
clear(key: string|SharedCreated, scopeName?: Prefix) {
169+
let keyStr!: string;
170+
let prefixStr!: string;
171+
if (typeof key === "string") {
172+
keyStr = key;
173+
prefixStr = scopeName || "_global";
174+
}
175+
else{
176+
keyStr = key.key;
177+
prefixStr = key.prefix;
178+
}
179+
this.sharedData.clear(keyStr, prefixStr);
160180
}
161181

162182
/**

src/context/SharedStatesContext.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
import {createContext, type PropsWithChildren, useContext, useMemo} from 'react';
2-
import type {NonEmptyString} from "../types";
2+
import type {Prefix} from "../types";
3+
import {random} from "../lib/utils";
34

45
export interface SharedStatesType {
56
scopeName: string
67
}
78

8-
const SharedStatesContext = createContext<SharedStatesType | undefined>(undefined);
9+
export const SharedStatesContext = createContext<SharedStatesType | undefined>(undefined);
910

10-
interface SharedStatesProviderProps<T extends string = string> extends PropsWithChildren {
11-
scopeName?: '__global' extends NonEmptyString<T> ? never : NonEmptyString<T>;
11+
interface SharedStatesProviderProps extends PropsWithChildren {
12+
scopeName?: Prefix;
1213
}
1314

14-
export const SharedStatesProvider = <T extends string = string>({ children, scopeName }: SharedStatesProviderProps<T>) => {
15+
export const SharedStatesProvider = ({ children, scopeName }: SharedStatesProviderProps) => {
1516
if (scopeName && scopeName.includes("//")) throw new Error("scopeName cannot contain '//'");
1617

17-
if (!scopeName) scopeName = useMemo(() => Math.random().toString(36).substring(2, 15) as NonNullable<SharedStatesProviderProps<T>['scopeName']>, []);
18+
if (!scopeName) scopeName = useMemo(() => random() as NonNullable<SharedStatesProviderProps['scopeName']>, []);
1819

1920
return (
2021
<SharedStatesContext.Provider value={{scopeName}}>

src/hooks/index.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
export {
22
useSharedState,
3-
sharedStatesApi
3+
sharedStatesApi,
4+
createSharedState,
5+
SharedStatesApi
46
} from "./use-shared-state";
7+
export type { SharedStateCreated } from "./use-shared-state";
58
export {
69
useSharedFunction,
7-
sharedFunctionsApi
10+
sharedFunctionsApi,
11+
createSharedFunction,
12+
SharedFunctionsApi
813
} from "./use-shared-function";
14+
export type { SharedFunctionStateReturn } from "./use-shared-function";
915
export {
1016
useSharedSubscription,
11-
sharedSubscriptionsApi
12-
} from "./use-shared-subscription";
17+
sharedSubscriptionsApi,
18+
createSharedSubscription,
19+
SharedSubscriptionsApi
20+
} from "./use-shared-subscription";
21+
export type { SharedSubscriptionStateReturn } from "./use-shared-subscription";

src/hooks/use-shared-function.ts

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import type {AFunction, Prefix} from "../types";
1+
import type {AFunction, Prefix, SharedCreated} from "../types";
22
import {useMemo, useSyncExternalStore} from "react";
33
import {SharedApi, SharedData} from "../SharedData";
44
import useShared from "./use-shared";
5-
import {ensureNonEmptyString} from "../lib/utils";
5+
import {ensureNonEmptyString, random} from "../lib/utils";
66

77
type SharedFunctionsState<T> = {
88
fnState: {
@@ -49,21 +49,60 @@ const sharedFunctionsData = new SharedFunctionsData();
4949

5050
export const sharedFunctionsApi = new SharedFunctionsApi(sharedFunctionsData);
5151

52-
export const useSharedFunction = <T, Args extends unknown[], S extends string = string>(key: S, fn: AFunction<T, Args>, scopeName?: Prefix) => {
52+
interface SharedFunctionCreated<T, Args extends unknown[]> extends SharedCreated{
53+
fn: AFunction<T, Args>
54+
}
55+
56+
export const createSharedFunction = <T, Args extends unknown[]>(fn: AFunction<T, Args>, scopeName?: Prefix): SharedFunctionCreated<T, Args> => {
57+
const prefix: Prefix = scopeName ?? scopeName ?? "_global";
5358

54-
key = ensureNonEmptyString(key);
55-
const {prefix} = useShared(scopeName);
59+
return {
60+
key: random(),
61+
prefix,
62+
fn,
63+
}
64+
}
65+
66+
export type SharedFunctionStateReturn<T, Args extends unknown[]> = {
67+
readonly state: NonNullable<SharedFunctionsState<T>['fnState']>,
68+
readonly trigger: (...args: Args) => void,
69+
readonly forceTrigger: (...args: Args) => void,
70+
readonly clear: () => void,
71+
}
72+
73+
export function useSharedFunction <T, Args extends unknown[], S extends string = string>(key: S, fn: AFunction<T, Args>, scopeName?: Prefix): SharedFunctionStateReturn<T, Args>;
74+
export function useSharedFunction <T, Args extends unknown[]>(sharedFunctionCreated: SharedFunctionCreated<T, Args>): SharedFunctionStateReturn<T, Args>;
75+
export function useSharedFunction <T, Args extends unknown[], S extends string = string>(
76+
key: S | SharedFunctionCreated<T, Args>,
77+
fn?: AFunction<T, Args>,
78+
scopeName?: Prefix
79+
): SharedFunctionStateReturn<T, Args> {
80+
81+
let keyStr: string;
82+
let fnVal!: AFunction<T, Args>;
83+
let scope: Prefix | undefined = scopeName;
84+
85+
if (typeof key !== "string") {
86+
const { key: key2, fn: fn2, prefix: prefix2 } = key;
87+
keyStr = key2;
88+
fnVal = fn2;
89+
scope = prefix2;
90+
} else {
91+
keyStr = ensureNonEmptyString(key);
92+
fnVal = fn as AFunction<T, Args>;
93+
}
94+
const {prefix} = useShared(scope);
5695

57-
sharedFunctionsData.init(key, prefix);
96+
sharedFunctionsData.init(keyStr, prefix);
5897

5998
const externalStoreSubscriber = useMemo<Parameters<typeof useSyncExternalStore<NonNullable<SharedFunctionsState<T>['fnState']>>>[0]>(
6099
() =>
61100
(listener) => {
62-
sharedFunctionsData.init(key, prefix);
63-
sharedFunctionsData.addListener(key, prefix, listener);
101+
sharedFunctionsData.init(keyStr, prefix);
102+
sharedFunctionsData.addListener(keyStr, prefix, listener);
64103

65104
return () => {
66-
sharedFunctionsData.removeListener(key, prefix, listener);
105+
sharedFunctionsData.removeListener(keyStr, prefix, listener);
67106
}
68107
},
69108
[]
@@ -72,27 +111,27 @@ export const useSharedFunction = <T, Args extends unknown[], S extends string =
72111
const externalStoreSnapshotGetter = useMemo<Parameters<typeof useSyncExternalStore<NonNullable<SharedFunctionsState<T>['fnState']>>>[1]>(
73112
() =>
74113
() =>
75-
sharedFunctionsData.get(key, prefix)!.fnState as NonNullable<SharedFunctionsState<T>['fnState']>,
114+
sharedFunctionsData.get(keyStr, prefix)!.fnState as NonNullable<SharedFunctionsState<T>['fnState']>,
76115
[]
77116
);
78117

79118
const state = useSyncExternalStore<NonNullable<SharedFunctionsState<T>['fnState']>>(externalStoreSubscriber, externalStoreSnapshotGetter);
80119

81120
const trigger = async (force: boolean, ...args: Args) => {
82-
const entry = sharedFunctionsData.get(key, prefix)!;
121+
const entry = sharedFunctionsData.get(keyStr, prefix)!;
83122
if (!force && (entry.fnState.isLoading || entry.fnState.results !== undefined)) return entry.fnState;
84123
entry.fnState = { ...entry.fnState, isLoading: true, error: undefined };
85124
entry.listeners.forEach(l => l());
86125
try {
87-
const results: Awaited<T> = await fn(...args);
126+
const results: Awaited<T> = await fnVal(...args);
88127
entry.fnState = { results, isLoading: false, error: undefined };
89128
} catch (error) {
90129
entry.fnState = { ...entry.fnState, isLoading: false, error };
91130
}
92131
entry.listeners.forEach(l => l());
93132
};
94133

95-
sharedFunctionsData.useEffect(key, prefix);
134+
sharedFunctionsData.useEffect(keyStr, prefix);
96135

97136
// noinspection JSUnusedGlobalSymbols
98137
return {
@@ -104,8 +143,8 @@ export const useSharedFunction = <T, Args extends unknown[], S extends string =
104143
void trigger(true, ...args);
105144
},
106145
clear: () => {
107-
sharedFunctionsData.clear(key, prefix);
108-
sharedFunctionsData.init(key, prefix);
146+
sharedFunctionsData.clear(keyStr, prefix);
147+
sharedFunctionsData.init(keyStr, prefix);
109148
}
110149
} as const;
111-
};
150+
}

src/hooks/use-shared-state.ts

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {useMemo, useSyncExternalStore} from "react";
2-
import type {AFunction, Prefix} from "../types";
2+
import type {AFunction, Prefix, SharedCreated} from "../types";
33
import {SharedApi, SharedData} from "../SharedData";
44
import useShared from "./use-shared";
5-
import {ensureNonEmptyString} from "../lib/utils";
5+
import {ensureNonEmptyString, random} from "../lib/utils";
66

77
class SharedStatesData extends SharedData<{
88
value: unknown
@@ -24,7 +24,7 @@ class SharedStatesData extends SharedData<{
2424
}
2525
}
2626

27-
class SharedStatesApi extends SharedApi<{
27+
export class SharedStatesApi extends SharedApi<{
2828
value: unknown
2929
}>{
3030
get<T, S extends string = string>(key: S, scopeName: Prefix = "_global") {
@@ -43,37 +43,67 @@ const sharedStatesData = new SharedStatesData();
4343

4444
export const sharedStatesApi = new SharedStatesApi(sharedStatesData);
4545

46+
export interface SharedStateCreated<T> extends SharedCreated{
47+
initialValue: T
48+
}
4649

50+
export const createSharedState = <T>(initialValue: T, scopeName?: Prefix): SharedStateCreated<T> => {
51+
const prefix: Prefix = scopeName ?? scopeName ?? "_global";
4752

48-
export const useSharedState = <T, S extends string = string>(key: S, value: T, scopeName?: Prefix) => {
53+
return {
54+
key: random(),
55+
prefix,
56+
initialValue,
57+
}
58+
}
59+
60+
export function useSharedState<T, S extends string>(key: S, initialValue: T, scopeName?: Prefix): readonly [T, (v: T | ((prev: T) => T)) => void];
61+
export function useSharedState<T>(sharedStateCreated: SharedStateCreated<T>): readonly [T, (v: T | ((prev: T) => T)) => void];
62+
export function useSharedState<T, S extends string>(
63+
key: S | SharedStateCreated<T>,
64+
initialValue?: T,
65+
scopeName?: Prefix
66+
): readonly [T, (v: T | ((prev: T) => T)) => void] {
67+
let keyStr: string;
68+
let initVal!: T;
69+
let scope: Prefix | undefined = scopeName;
70+
71+
if (typeof key !== "string") {
72+
const { key: key2, initialValue: value2, prefix: prefix2 } = key;
73+
keyStr = key2;
74+
initVal = value2;
75+
scope = prefix2;
76+
} else {
77+
keyStr = ensureNonEmptyString(key);
78+
initVal = initialValue as T;
79+
}
4980

50-
key = ensureNonEmptyString(key);
51-
const {prefix} = useShared(scopeName);
81+
const {prefix} = useShared(scope);
5282

53-
sharedStatesData.init(key, prefix, value);
83+
sharedStatesData.init(keyStr, prefix, initVal);
5484

5585
const externalStoreSubscriber = useMemo<Parameters<typeof useSyncExternalStore>[0]>(() => (listener) => {
56-
sharedStatesData.init(key, prefix, value);
57-
sharedStatesData.addListener(key, prefix, listener);
86+
sharedStatesData.init(keyStr, prefix, initialValue);
87+
sharedStatesData.addListener(keyStr, prefix, listener);
5888

5989
return () => {
60-
sharedStatesData.removeListener(key, prefix, listener);
90+
sharedStatesData.removeListener(keyStr, prefix, listener);
6191
}
6292
}, []);
6393

64-
const externalStoreSnapshotGetter = useMemo(() => () => sharedStatesData.get(key, prefix)?.value as T, []);
94+
const externalStoreSnapshotGetter = useMemo(() => () => sharedStatesData.get(keyStr, prefix)?.value as T, []);
6595

6696
const dataValue = useSyncExternalStore(externalStoreSubscriber, externalStoreSnapshotGetter);
6797

6898
const setData = (newValueOrCallbackToNewValue: T|((prev: T) => T)) => {
69-
const newValue = (typeof newValueOrCallbackToNewValue === "function") ? (newValueOrCallbackToNewValue as (prev: T) => T)(sharedStatesData.get(key, prefix)?.value as T) : newValueOrCallbackToNewValue
99+
const newValue = (typeof newValueOrCallbackToNewValue === "function") ? (newValueOrCallbackToNewValue as (prev: T) => T)(sharedStatesData.get(keyStr, prefix)?.value as T) : newValueOrCallbackToNewValue
70100
if (newValue !== dataValue) {
71-
sharedStatesData.setValue(key, prefix, newValue);
72-
sharedStatesData.callListeners(key, prefix);
101+
sharedStatesData.setValue(keyStr, prefix, newValue);
102+
sharedStatesData.callListeners(keyStr, prefix);
73103
}
74104
}
75105

76-
sharedStatesData.useEffect(key, prefix);
106+
sharedStatesData.useEffect(keyStr, prefix);
77107

78108
return [
79109
dataValue,

0 commit comments

Comments
 (0)