From a17fd8139988dd579356d166d6ef9e3eab96eabd Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Tue, 30 Apr 2024 11:43:15 +0530 Subject: [PATCH 01/17] remove serverValue as it has to be always same as client value we are not expecting functions using client side API here --- lib/r18gs/src/index.ts | 28 ++++++++++++++-- .../{persist-and-sync.ts => persist.ts} | 2 +- lib/r18gs/src/use-rgs.ts | 28 ---------------- lib/r18gs/src/utils.ts | 32 +++++++------------ lib/r18gs/src/with-plugins.ts | 10 ++---- lib/r18gs/tests/create.test.tsx | 4 +-- 6 files changed, 44 insertions(+), 60 deletions(-) rename lib/r18gs/src/plugins/{persist-and-sync.ts => persist.ts} (96%) delete mode 100644 lib/r18gs/src/use-rgs.ts diff --git a/lib/r18gs/src/index.ts b/lib/r18gs/src/index.ts index 3d70ae81..8ba7505d 100644 --- a/lib/r18gs/src/index.ts +++ b/lib/r18gs/src/index.ts @@ -1,4 +1,28 @@ -import useRGS from "./use-rgs"; +/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style -- as ! operator is forbidden by eslint*/ +import { createHook, createSetter, createSubcriber, globalRGS } from "./utils"; + +import type { SetStateAction } from "./utils"; + export type { SetterArgType, SetStateAction, Plugin } from "./utils"; -export default useRGS; +/** + * Use this hook similar to `useState` hook. + * The difference is that you need to pass a + * unique key - unique across the app to make + * this state accessible to all client components. + * + * @example + * ```tsx + * const [state, setState] = useRGS("counter", 1); + * ``` + * + * @param key - Unique key to identify the store. + * @param value - Initial value of the store. + * @returns - A tuple (Ordered sequance of values) containing the state and a function to set the state. + */ +export default function useRGS(key: string, value?: T): [T, SetStateAction] { + /** Initialize the named store when invoked for the first time. */ + if (!globalRGS[key]) globalRGS[key] = [value, [], createSetter(key), createSubcriber(key)]; + + return createHook(key); +} diff --git a/lib/r18gs/src/plugins/persist-and-sync.ts b/lib/r18gs/src/plugins/persist.ts similarity index 96% rename from lib/r18gs/src/plugins/persist-and-sync.ts rename to lib/r18gs/src/plugins/persist.ts index 9d04c9cf..4c10e99e 100644 --- a/lib/r18gs/src/plugins/persist-and-sync.ts +++ b/lib/r18gs/src/plugins/persist.ts @@ -7,7 +7,7 @@ import { Plugin } from ".."; */ function persistAndSyncPlugin(): Plugin { return { - init(key, value, _, mutate) { + init(key, _, mutate) { if (typeof window === "undefined") return; const persistedValue = localStorage.getItem(key); const newVal = JSON.parse(persistedValue || "{}").val; diff --git a/lib/r18gs/src/use-rgs.ts b/lib/r18gs/src/use-rgs.ts deleted file mode 100644 index 11070222..00000000 --- a/lib/r18gs/src/use-rgs.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style -- as ! operator is forbidden by eslint*/ -import { createHook, createSetter, createSubcriber, globalRGS } from "./utils"; - -import type { SetStateAction } from "./utils"; - -/** - * Use this hook similar to `useState` hook. - * The difference is that you need to pass a - * unique key - unique across the app to make - * this state accessible to all client components. - * - * @example - * ```tsx - * const [state, setState] = useRGS("counter", 1); - * ``` - * - * @param key - Unique key to identify the store. - * @param value - Initial value of the store. - * @param serverValue - Server value of the store. - * @returns - A tuple (Ordered sequance of values) containing the state and a function to set the state. - */ -export default function useRGS(key: string, value?: T, serverValue?: T): [T, SetStateAction] { - /** Initialize the named store when invoked for the first time. */ - if (!globalRGS[key]) - globalRGS[key] = [value, serverValue, [], createSetter(key), createSubcriber(key)]; - - return createHook(key); -} diff --git a/lib/r18gs/src/utils.ts b/lib/r18gs/src/utils.ts index 0adcd1a0..cc82698e 100644 --- a/lib/r18gs/src/utils.ts +++ b/lib/r18gs/src/utils.ts @@ -9,8 +9,8 @@ export type SetStateAction = (value: SetterArgType) => void; /** * This is a hack to reduce lib size + readability + not encouraging direct access to globalThis */ -const [VALUE, SERVER_VALUE, LISTENERS, SETTER, SUBSCRIBER] = [0, 1, 2, 3, 4]; -type RGS = [unknown, unknown, Listener[], SetStateAction, Subscriber]; +const [VALUE, LISTENERS, SETTER, SUBSCRIBER] = [0, 1, 2, 3, 4]; +type RGS = [unknown, Listener[], SetStateAction, Subscriber]; declare global { // eslint-disable-next-line no-var -- var required for global declaration. @@ -54,44 +54,36 @@ export function createHook(key: string): [T, SetStateAction] { const setRGState = rgs[SETTER] as SetStateAction; /** Function to get snapshot of the state. */ const getSnap = () => rgs[VALUE] as T; - /** Function to get server snapshot. Returns server value is provided else the default value. */ - const getServerSnap = () => (rgs[SERVER_VALUE] ?? rgs[VALUE]) as T; - const val = useSyncExternalStore(rgs[SUBSCRIBER] as Subscriber, getSnap, getServerSnap); + const val = useSyncExternalStore(rgs[SUBSCRIBER] as Subscriber, getSnap); return [val, setRGState]; } -type Mutate = (value?: T, serverValue?: T) => void; +type Mutate = (value?: T) => void; export type Plugin = { - init?: (key: string, value: T | undefined, serverValue: T | undefined, mutate: Mutate) => void; - onChange?: (key: string, value?: T, serverValue?: T) => void; + init?: (key: string, value: T | undefined, mutate: Mutate) => void; + onChange?: (key: string, value?: T) => void; }; let allExtentionsInitialized = false; /** Initialize extestions - wait for previous plugins's promise to be resolved before processing next */ async function initPlugins(key: string, plugins: Plugin[]) { const rgs = globalRGS[key] as RGS; - /** Mutate function to update the value and server value */ - const mutate: Mutate = (newValue, newServerValue) => { + /** Mutate function to update the value */ + const mutate: Mutate = newValue => { rgs[VALUE] = newValue; - rgs[SERVER_VALUE] = newServerValue; triggerListeners(rgs); }; for (const plugin of plugins) { /** Next plugins initializer will get the new value if updated by previous one */ - await plugin.init?.(key, rgs[VALUE] as T, rgs[SERVER_VALUE] as T, mutate); + await plugin.init?.(key, rgs[VALUE] as T, mutate); } allExtentionsInitialized = true; } /** Initialize the named store when invoked for the first time. */ -export function initWithPlugins( - key: string, - value?: T, - serverValue?: T, - plugins: Plugin[] = [], -) { +export function initWithPlugins(key: string, value?: T, plugins: Plugin[] = []) { /** setter function to set the state. */ const setterWithPlugins: SetStateAction = val => { /** Do not allow mutating the store before all extentions are initialized */ @@ -99,9 +91,9 @@ export function initWithPlugins( const rgs = globalRGS[key] as RGS; rgs[VALUE] = val instanceof Function ? val(rgs[VALUE] as T) : val; triggerListeners(rgs); - plugins.forEach(plugin => plugin.onChange?.(key, rgs[VALUE] as T, rgs[SERVER_VALUE] as T)); + plugins.forEach(plugin => plugin.onChange?.(key, rgs[VALUE] as T)); }; - globalRGS[key] = [value, serverValue, [], setterWithPlugins, createSubcriber(key)]; + globalRGS[key] = [value, [], setterWithPlugins, createSubcriber(key)]; initPlugins(key, plugins); } diff --git a/lib/r18gs/src/with-plugins.ts b/lib/r18gs/src/with-plugins.ts index 3a5042ff..6f0bc232 100644 --- a/lib/r18gs/src/with-plugins.ts +++ b/lib/r18gs/src/with-plugins.ts @@ -16,17 +16,15 @@ import type { Plugin, SetStateAction } from "./utils"; * * @param key - Unique key to identify the store. * @param value - Initial value of the store. - * @param serverValue - Server value of the store. * @param plugins - Plugins to be applied to the store. * @returns - A tuple (Ordered sequance of values) containing the state and a function to set the state. */ export function useRGSWithPlugins( key: string, value?: T, - serverValue?: T, plugins?: Plugin[], ): [T, SetStateAction] { - if (!globalRGS[key]) initWithPlugins(key, value, serverValue, plugins); + if (!globalRGS[key]) initWithPlugins(key, value, plugins); return createHook(key); } @@ -39,7 +37,7 @@ export function useRGSWithPlugins( * @example * ```tsx * // in hook file - * export const useRGS = create(key, value, serverValue, plugins); + * export const useRGS = create(key, value, plugins); * * // in component file * const [state, setState] = useRGS(); @@ -47,15 +45,13 @@ export function useRGSWithPlugins( * * @param key - Unique key to identify the store. * @param value - Initial value of the store. - * @param serverValue - Server value of the store. * @param plugins - Plugins to be applied to the store. * @returns - A hook funciton that returns a tuple (Ordered sequance of values) containing the state and a function to set the state. */ export function create( key: string, value?: T, - serverValue?: T, plugins?: Plugin[], ): () => [T, SetStateAction] { - return () => useRGSWithPlugins(key, value, serverValue, plugins); + return () => useRGSWithPlugins(key, value, plugins); } diff --git a/lib/r18gs/tests/create.test.tsx b/lib/r18gs/tests/create.test.tsx index 72615c0e..819b07ca 100644 --- a/lib/r18gs/tests/create.test.tsx +++ b/lib/r18gs/tests/create.test.tsx @@ -1,12 +1,12 @@ import { describe, test } from "vitest"; import { act, fireEvent, render, screen } from "@testing-library/react"; import { create } from "../src/with-plugins"; -import persistAndSyncPlugin from "../src/plugins/persist-and-sync"; +import persistAndSyncPlugin from "../src/plugins/persist"; import { ChangeEvent, useCallback } from "react"; const COUNTER_RGS_KEY = "count"; -const useMyRGS = create(COUNTER_RGS_KEY, 0, 0, [persistAndSyncPlugin()]); +const useMyRGS = create(COUNTER_RGS_KEY, 0, [persistAndSyncPlugin()]); const TESTID_INPUT = "in1"; const TESTID_DISPLAY = "d1"; From be90d74bf620f2df4e562356304bf6270ff3bc70 Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Tue, 30 Apr 2024 11:43:23 +0530 Subject: [PATCH 02/17] refactor --- lib/r18gs/src/index.ts | 2 +- lib/r18gs/src/utils.ts | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/r18gs/src/index.ts b/lib/r18gs/src/index.ts index 8ba7505d..baf1b8d6 100644 --- a/lib/r18gs/src/index.ts +++ b/lib/r18gs/src/index.ts @@ -24,5 +24,5 @@ export default function useRGS(key: string, value?: T): [T, SetStateAction /** Initialize the named store when invoked for the first time. */ if (!globalRGS[key]) globalRGS[key] = [value, [], createSetter(key), createSubcriber(key)]; - return createHook(key); + return createHook(key); } diff --git a/lib/r18gs/src/utils.ts b/lib/r18gs/src/utils.ts index cc82698e..04e83c14 100644 --- a/lib/r18gs/src/utils.ts +++ b/lib/r18gs/src/utils.ts @@ -50,13 +50,8 @@ export function createSetter(key: string): SetStateAction { /** Extract coomon create hook logic to utils */ export function createHook(key: string): [T, SetStateAction] { const rgs = globalRGS[key] as RGS; - /** Function to set the state. */ - const setRGState = rgs[SETTER] as SetStateAction; - /** Function to get snapshot of the state. */ - const getSnap = () => rgs[VALUE] as T; - - const val = useSyncExternalStore(rgs[SUBSCRIBER] as Subscriber, getSnap); - return [val, setRGState]; + const val = useSyncExternalStore(rgs[SUBSCRIBER] as Subscriber, () => rgs[VALUE] as T); + return [val, rgs[SETTER] as SetStateAction]; } type Mutate = (value?: T) => void; From 825465d4db4292d79bf73b4bd4a5fde546642e51 Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Tue, 30 Apr 2024 13:13:24 +0530 Subject: [PATCH 03/17] improve minified version --- lib/r18gs/src/utils.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/r18gs/src/utils.ts b/lib/r18gs/src/utils.ts index 04e83c14..1371d86b 100644 --- a/lib/r18gs/src/utils.ts +++ b/lib/r18gs/src/utils.ts @@ -30,10 +30,9 @@ function triggerListeners(rgs: RGS) { export function createSubcriber(key: string): Subscriber { return listener => { const rgs = globalRGS[key] as RGS; - const listeners = rgs[LISTENERS] as Listener[]; - listeners.push(listener); + (rgs[LISTENERS] as Listener[]).push(listener); return () => { - rgs[LISTENERS] = listeners.filter(l => l !== listener); + rgs[LISTENERS] = (rgs[LISTENERS] as Listener[]).filter(l => l !== listener); }; }; } @@ -43,7 +42,7 @@ export function createSetter(key: string): SetStateAction { return val => { const rgs = globalRGS[key] as RGS; rgs[VALUE] = val instanceof Function ? val(rgs[VALUE] as T) : val; - triggerListeners(rgs); + (rgs[LISTENERS] as Listener[]).forEach(listener => listener()); }; } From 7e8fb39ad2db4bf9e4d77e534b698e539eadab4e Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Tue, 30 Apr 2024 13:40:49 +0530 Subject: [PATCH 04/17] Add options to persist plugin --- lib/r18gs/src/plugins/index.ts | 1 + lib/r18gs/src/plugins/persist.ts | 57 ++++++++++++++++++++++++++------ lib/r18gs/tests/create.test.tsx | 4 +-- 3 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 lib/r18gs/src/plugins/index.ts diff --git a/lib/r18gs/src/plugins/index.ts b/lib/r18gs/src/plugins/index.ts new file mode 100644 index 00000000..8818f742 --- /dev/null +++ b/lib/r18gs/src/plugins/index.ts @@ -0,0 +1 @@ +export * from "./persist"; diff --git a/lib/r18gs/src/plugins/persist.ts b/lib/r18gs/src/plugins/persist.ts index 4c10e99e..80d743eb 100644 --- a/lib/r18gs/src/plugins/persist.ts +++ b/lib/r18gs/src/plugins/persist.ts @@ -1,31 +1,66 @@ import { Plugin } from ".."; +interface PersistOptions { + /** @defaultValue true */ + sync?: boolean; + /** @defaultValue local */ + storage?: "local" | "session" | "cookie"; +} + +/** get stored item */ +function getItem(key: string, options?: PersistOptions) { + switch (options?.storage) { + case "cookie": + const cookies = document.cookie.split("; "); + const cookie = cookies.find(c => c.startsWith(key)); + return cookie?.split("=")[1]; + case "session": + return sessionStorage.getItem(key); + default: + return localStorage.getItem(key); + } +} + +/** set item to persistant store */ +function setItem(key: string, value: string, options?: PersistOptions) { + switch (options?.storage) { + case "cookie": + document.cookie = `${key}=${value}; max-age=31536000; SameSite=Strict;`; + if (options.sync ?? true) sessionStorage.setItem(key, value); + return; + case "session": + sessionStorage.setItem(key, value); + return; + default: + localStorage.setItem(key, value); + } +} /** * A plugin that persists and syncs RGS store between tabs. * * @returns A plugin that persists and syncs a value between tabs. */ -function persistAndSyncPlugin(): Plugin { +export function persist(options?: PersistOptions): Plugin { return { init(key, _, mutate) { if (typeof window === "undefined") return; - const persistedValue = localStorage.getItem(key); + const persistedValue = getItem(key, options); const newVal = JSON.parse(persistedValue || "{}").val; if (newVal) mutate(newVal); - addEventListener("storage", e => { - if (e.key === key && e.newValue) { - const newVal = JSON.parse(e.newValue).val; - if (newVal !== undefined) mutate(newVal); - } - }); + if (options?.sync ?? true) { + addEventListener("storage", e => { + if (e.key === key && e.newValue) { + const newVal = JSON.parse(e.newValue).val; + if (newVal !== undefined) mutate(newVal); + } + }); + } }, onChange(key, value) { if (typeof window !== "undefined") { - localStorage.setItem(key, JSON.stringify({ val: value })); + setItem(key, JSON.stringify({ val: value }), options); } }, }; } - -export default persistAndSyncPlugin; diff --git a/lib/r18gs/tests/create.test.tsx b/lib/r18gs/tests/create.test.tsx index 819b07ca..01935d79 100644 --- a/lib/r18gs/tests/create.test.tsx +++ b/lib/r18gs/tests/create.test.tsx @@ -1,12 +1,12 @@ import { describe, test } from "vitest"; import { act, fireEvent, render, screen } from "@testing-library/react"; import { create } from "../src/with-plugins"; -import persistAndSyncPlugin from "../src/plugins/persist"; +import { persist } from "../src/plugins/persist"; import { ChangeEvent, useCallback } from "react"; const COUNTER_RGS_KEY = "count"; -const useMyRGS = create(COUNTER_RGS_KEY, 0, [persistAndSyncPlugin()]); +const useMyRGS = create(COUNTER_RGS_KEY, 0, [persist()]); const TESTID_INPUT = "in1"; const TESTID_DISPLAY = "d1"; From 79f4127824028ee5b59522a67e97e24d862c37b9 Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Tue, 30 Apr 2024 13:48:31 +0530 Subject: [PATCH 05/17] Changelog --- examples/nextjs/CHANGELOG.md | 11 +++++++++++ examples/nextjs/package.json | 2 +- examples/remix/CHANGELOG.md | 11 +++++++++++ examples/remix/package.json | 2 +- examples/vite/CHANGELOG.md | 11 +++++++++++ examples/vite/package.json | 2 +- lib/r18gs/CHANGELOG.md | 15 +++++++++++++++ lib/r18gs/package.json | 2 +- 8 files changed, 52 insertions(+), 4 deletions(-) diff --git a/examples/nextjs/CHANGELOG.md b/examples/nextjs/CHANGELOG.md index 20c63a52..3e4a358e 100644 --- a/examples/nextjs/CHANGELOG.md +++ b/examples/nextjs/CHANGELOG.md @@ -1,5 +1,16 @@ # nextjs-example +## 0.0.12 + +### Patch Changes + +- Updated dependencies +- Updated dependencies +- Updated dependencies +- Updated dependencies + - r18gs@1.0.0 + - shared-ui@0.0.0 + ## 0.0.11 ### Patch Changes diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index b92f5d3f..3d0ca9d6 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "nextjs-example", - "version": "0.0.11", + "version": "0.0.12", "private": true, "scripts": { "dev": "next dev", diff --git a/examples/remix/CHANGELOG.md b/examples/remix/CHANGELOG.md index f9cd871d..d5005680 100644 --- a/examples/remix/CHANGELOG.md +++ b/examples/remix/CHANGELOG.md @@ -1,5 +1,16 @@ # remix-example +## 0.0.12 + +### Patch Changes + +- Updated dependencies +- Updated dependencies +- Updated dependencies +- Updated dependencies + - r18gs@1.0.0 + - shared-ui@0.0.0 + ## 0.0.11 ### Patch Changes diff --git a/examples/remix/package.json b/examples/remix/package.json index 015a2b3c..95be728d 100644 --- a/examples/remix/package.json +++ b/examples/remix/package.json @@ -1,6 +1,6 @@ { "name": "remix-example", - "version": "0.0.11", + "version": "0.0.12", "private": true, "sideEffects": false, "type": "module", diff --git a/examples/vite/CHANGELOG.md b/examples/vite/CHANGELOG.md index 0604d258..8952f6da 100644 --- a/examples/vite/CHANGELOG.md +++ b/examples/vite/CHANGELOG.md @@ -1,5 +1,16 @@ # vite-example +## 0.0.12 + +### Patch Changes + +- Updated dependencies +- Updated dependencies +- Updated dependencies +- Updated dependencies + - r18gs@1.0.0 + - shared-ui@0.0.0 + ## 0.0.11 ### Patch Changes diff --git a/examples/vite/package.json b/examples/vite/package.json index d1ce5450..ff8b5748 100644 --- a/examples/vite/package.json +++ b/examples/vite/package.json @@ -1,7 +1,7 @@ { "name": "vite-example", "private": true, - "version": "0.0.11", + "version": "0.0.12", "type": "module", "scripts": { "dev": "vite --port 3001", diff --git a/lib/r18gs/CHANGELOG.md b/lib/r18gs/CHANGELOG.md index b0319ffa..ecb68077 100644 --- a/lib/r18gs/CHANGELOG.md +++ b/lib/r18gs/CHANGELOG.md @@ -1,5 +1,20 @@ # r18gs +## 1.0.0 + +### Major Changes + +- Removed `serverValue` field as it should always be same as `value`. For more details please check out documentation of useSyncExternalStore hook provided by react. + +### Minor Changes + +- Added persist plugin to create state that is persisted and synced between browser contexts. +- Added `useRGSWithPlugins` hook to support extending the store functionality with plugins. + +### Patch Changes + +- Refactored to reduce bundle size and improve stability and reduce likelyhood of bugs. + ## 0.2.0 ### Minor Changes diff --git a/lib/r18gs/package.json b/lib/r18gs/package.json index f579d2f8..e8675015 100644 --- a/lib/r18gs/package.json +++ b/lib/r18gs/package.json @@ -2,7 +2,7 @@ "name": "r18gs", "author": "Mayank Kumar Chaudhari ", "private": false, - "version": "0.2.0", + "version": "1.0.0", "description": "A simple yet elegant, light weight, react18 global store to replace Zustand for better tree shaking.", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 1ee111c5db5b638649c35501c442498e7f06c7b8 Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Tue, 30 Apr 2024 13:55:31 +0530 Subject: [PATCH 06/17] Avoid using lexical declarations in case clauses --- lib/r18gs/src/plugins/persist.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/r18gs/src/plugins/persist.ts b/lib/r18gs/src/plugins/persist.ts index 80d743eb..b673c8f3 100644 --- a/lib/r18gs/src/plugins/persist.ts +++ b/lib/r18gs/src/plugins/persist.ts @@ -9,11 +9,11 @@ interface PersistOptions { /** get stored item */ function getItem(key: string, options?: PersistOptions) { + const cookie = document.cookie.split("; ").find(c => c.startsWith(key)); switch (options?.storage) { - case "cookie": - const cookies = document.cookie.split("; "); - const cookie = cookies.find(c => c.startsWith(key)); + case "cookie": { return cookie?.split("=")[1]; + } case "session": return sessionStorage.getItem(key); default: From 9f112ff0f372f58af913bf9c23de61d303b770ef Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Tue, 30 Apr 2024 14:26:05 +0530 Subject: [PATCH 07/17] Update unit tests --- lib/r18gs/src/plugins/persist.ts | 5 +- lib/r18gs/tests/create.test.tsx | 8 +-- lib/r18gs/tests/plugins/persist.test.tsx | 84 ++++++++++++++++++++++++ lib/r18gs/tests/use-rgs.test.tsx | 10 +-- lib/r18gs/vitest.config.ts | 1 - 5 files changed, 95 insertions(+), 13 deletions(-) create mode 100644 lib/r18gs/tests/plugins/persist.test.tsx diff --git a/lib/r18gs/src/plugins/persist.ts b/lib/r18gs/src/plugins/persist.ts index b673c8f3..d364eb4b 100644 --- a/lib/r18gs/src/plugins/persist.ts +++ b/lib/r18gs/src/plugins/persist.ts @@ -1,6 +1,6 @@ import { Plugin } from ".."; -interface PersistOptions { +export interface PersistOptions { /** @defaultValue true */ sync?: boolean; /** @defaultValue local */ @@ -11,9 +11,8 @@ interface PersistOptions { function getItem(key: string, options?: PersistOptions) { const cookie = document.cookie.split("; ").find(c => c.startsWith(key)); switch (options?.storage) { - case "cookie": { + case "cookie": return cookie?.split("=")[1]; - } case "session": return sessionStorage.getItem(key); default: diff --git a/lib/r18gs/tests/create.test.tsx b/lib/r18gs/tests/create.test.tsx index 01935d79..5d1ab45c 100644 --- a/lib/r18gs/tests/create.test.tsx +++ b/lib/r18gs/tests/create.test.tsx @@ -5,12 +5,11 @@ import { persist } from "../src/plugins/persist"; import { ChangeEvent, useCallback } from "react"; const COUNTER_RGS_KEY = "count"; - -const useMyRGS = create(COUNTER_RGS_KEY, 0, [persist()]); - const TESTID_INPUT = "in1"; const TESTID_DISPLAY = "d1"; +const useMyRGS = create(COUNTER_RGS_KEY, 0, [persist({ storage: "cookie" })]); + function Component1() { const [count, setCount] = useMyRGS(); const handleChange = useCallback( @@ -28,7 +27,6 @@ function Component1() { function Component2() { const [count] = useMyRGS(); - console.log("c2", { count }); return

{count}

; } @@ -40,7 +38,7 @@ describe("React18GlobalStore", () => { await new Promise(resolve => setTimeout(resolve, 100)); await act(() => fireEvent.input(screen.getByTestId(TESTID_INPUT), { target: { value: 5 } })); expect(screen.getByTestId(TESTID_DISPLAY).textContent).toBe("5"); - expect(JSON.parse(localStorage.getItem(COUNTER_RGS_KEY) ?? "{}").val).toBe(5); + expect(JSON.parse(sessionStorage.getItem(COUNTER_RGS_KEY) ?? "{}").val).toBe(5); }); test("storage event", async ({ expect }) => { diff --git a/lib/r18gs/tests/plugins/persist.test.tsx b/lib/r18gs/tests/plugins/persist.test.tsx new file mode 100644 index 00000000..4a15bde8 --- /dev/null +++ b/lib/r18gs/tests/plugins/persist.test.tsx @@ -0,0 +1,84 @@ +import { describe, test } from "vitest"; +import { act, fireEvent, render, screen } from "@testing-library/react"; +import { create } from "../../src/with-plugins"; +import { PersistOptions, persist } from "../../src/plugins"; +import { ChangeEvent, useCallback } from "react"; + +const TESTID_INPUT = "in1"; +const TESTID_DISPLAY = "d1"; + +const persistOptions: PersistOptions[] = [ + {}, + { storage: "local" }, + { storage: "session" }, + { storage: "cookie" }, + { sync: false }, +]; + +describe("React18GlobalStore", () => { + persistOptions.forEach((options, i) => { + const COUNTER_RGS_KEY = "count" + i; + const useMyRGS = create(COUNTER_RGS_KEY, 0, [persist(options)]); + + function Component1() { + const [count, setCount] = useMyRGS(); + const handleChange = useCallback( + (e: ChangeEvent) => { + setCount(parseInt(e.target.value)); + }, + [setCount], + ); + return ( +
+ +
+ ); + } + + function Component2() { + const [count] = useMyRGS(); + return

{count}

; + } + + test( + "check state update to multiple components: " + JSON.stringify(options), + async ({ expect }) => { + render(); + render(); + /** Await and allow for the state to update from localStorate */ + await new Promise(resolve => setTimeout(resolve, 100)); + await act(() => + fireEvent.input(screen.getByTestId(TESTID_INPUT), { target: { value: 5 } }), + ); + expect(screen.getByTestId(TESTID_DISPLAY).textContent).toBe("5"); + console.log("sessionStorage --- ", sessionStorage.getItem(COUNTER_RGS_KEY), options); + console.log("localStorage --- ", localStorage.getItem(COUNTER_RGS_KEY)); + expect( + JSON.parse( + ((options.storage ?? "local") === "local" ? localStorage : sessionStorage).getItem( + COUNTER_RGS_KEY, + ) ?? "{}", + ).val, + ).toBe(5); + }, + ); + + if (options.sync ?? true) { + test("storage event", async ({ expect }) => { + render(); + render(); + /** Await and allow for the state to update from localStorate */ + await new Promise(resolve => setTimeout(resolve, 100)); + await act(() => + window.dispatchEvent( + new StorageEvent("storage", { + key: COUNTER_RGS_KEY, + newValue: JSON.stringify({ val: 15 }), + }), + ), + ); + expect(screen.getByTestId(TESTID_DISPLAY).textContent).toBe("15"); + }); + } + }); +}); diff --git a/lib/r18gs/tests/use-rgs.test.tsx b/lib/r18gs/tests/use-rgs.test.tsx index ecd7751e..ceec4e18 100644 --- a/lib/r18gs/tests/use-rgs.test.tsx +++ b/lib/r18gs/tests/use-rgs.test.tsx @@ -4,6 +4,8 @@ import useRGS from "../src"; import { ChangeEvent, useCallback } from "react"; const COUNT_RGS_KEY = "count"; +const TESTID_INPUT = "in1"; +const TESTID_DISPLAY = "d1"; function Component1() { const [count, setCount] = useRGS(COUNT_RGS_KEY, 0); @@ -15,21 +17,21 @@ function Component1() { ); return (
- +
); } function Component2() { const [count] = useRGS(COUNT_RGS_KEY); - return

{count}

; + return

{count}

; } describe("React18GlobalStore", () => { test("check state update to multiple components", async ({ expect }) => { render(); render(); - await act(() => fireEvent.input(screen.getByTestId("input"), { target: { value: 5 } })); - expect(screen.getByTestId("display").textContent).toBe("5"); + await act(() => fireEvent.input(screen.getByTestId(TESTID_INPUT), { target: { value: 5 } })); + expect(screen.getByTestId(TESTID_DISPLAY).textContent).toBe("5"); }); }); diff --git a/lib/r18gs/vitest.config.ts b/lib/r18gs/vitest.config.ts index c264278a..9b0a01dc 100644 --- a/lib/r18gs/vitest.config.ts +++ b/lib/r18gs/vitest.config.ts @@ -11,7 +11,6 @@ export default defineConfig({ setupFiles: [], coverage: { include: ["src/**"], - exclude: ["src/**/index.ts", "src/**/declaration.d.ts"], reporter: ["text", "json", "clover", "html"], }, }, From 17d705f9a6079b21639699bbf72a84c5ea230809 Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Tue, 30 Apr 2024 14:29:01 +0530 Subject: [PATCH 08/17] Fix anti patterns --- lib/r18gs/tests/plugins/persist.test.tsx | 43 +++++++++++------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/lib/r18gs/tests/plugins/persist.test.tsx b/lib/r18gs/tests/plugins/persist.test.tsx index 4a15bde8..57d2f2de 100644 --- a/lib/r18gs/tests/plugins/persist.test.tsx +++ b/lib/r18gs/tests/plugins/persist.test.tsx @@ -17,7 +17,7 @@ const persistOptions: PersistOptions[] = [ describe("React18GlobalStore", () => { persistOptions.forEach((options, i) => { - const COUNTER_RGS_KEY = "count" + i; + const COUNTER_RGS_KEY = `count-${i}`; const useMyRGS = create(COUNTER_RGS_KEY, 0, [persist(options)]); function Component1() { @@ -40,28 +40,25 @@ describe("React18GlobalStore", () => { return

{count}

; } - test( - "check state update to multiple components: " + JSON.stringify(options), - async ({ expect }) => { - render(); - render(); - /** Await and allow for the state to update from localStorate */ - await new Promise(resolve => setTimeout(resolve, 100)); - await act(() => - fireEvent.input(screen.getByTestId(TESTID_INPUT), { target: { value: 5 } }), - ); - expect(screen.getByTestId(TESTID_DISPLAY).textContent).toBe("5"); - console.log("sessionStorage --- ", sessionStorage.getItem(COUNTER_RGS_KEY), options); - console.log("localStorage --- ", localStorage.getItem(COUNTER_RGS_KEY)); - expect( - JSON.parse( - ((options.storage ?? "local") === "local" ? localStorage : sessionStorage).getItem( - COUNTER_RGS_KEY, - ) ?? "{}", - ).val, - ).toBe(5); - }, - ); + test(`check state update to multiple components: ${JSON.stringify(options)}`, async ({ + expect, + }) => { + render(); + render(); + /** Await and allow for the state to update from localStorate */ + await new Promise(resolve => setTimeout(resolve, 100)); + await act(() => fireEvent.input(screen.getByTestId(TESTID_INPUT), { target: { value: 5 } })); + expect(screen.getByTestId(TESTID_DISPLAY).textContent).toBe("5"); + console.log("sessionStorage --- ", sessionStorage.getItem(COUNTER_RGS_KEY), options); + console.log("localStorage --- ", localStorage.getItem(COUNTER_RGS_KEY)); + expect( + JSON.parse( + ((options.storage ?? "local") === "local" ? localStorage : sessionStorage).getItem( + COUNTER_RGS_KEY, + ) ?? "{}", + ).val, + ).toBe(5); + }); if (options.sync ?? true) { test("storage event", async ({ expect }) => { From 19b6321272b2bf34b400bbe401a200dec34214bf Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Tue, 30 Apr 2024 15:31:58 +0530 Subject: [PATCH 09/17] Add polar to funding --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 47c926b3..95f5d426 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,7 @@ # These are supported funding model platforms github: [mayank1513] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +polar: mayank1513 patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username From 2b22639b38f8a774aa95a83b79da91b4d966698a Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Tue, 30 Apr 2024 15:45:04 +0530 Subject: [PATCH 10/17] Update typedoc config --- lib/r18gs/package.json | 4 ++++ lib/r18gs/tsconfig.doc.json | 5 ----- lib/r18gs/typedoc.config.js | 9 ++++++++- 3 files changed, 12 insertions(+), 6 deletions(-) delete mode 100644 lib/r18gs/tsconfig.doc.json diff --git a/lib/r18gs/package.json b/lib/r18gs/package.json index e8675015..8f2037e7 100644 --- a/lib/r18gs/package.json +++ b/lib/r18gs/package.json @@ -42,6 +42,10 @@ "tsconfig": "workspace:*", "tsup": "^8.0.2", "typedoc": "^0.25.13", + "typedoc-plugin-mdn-links": "^3.1.23", + "typedoc-plugin-missing-exports": "^2.2.0", + "typedoc-plugin-rename-defaults": "^0.7.0", + "typedoc-plugin-zod": "^1.1.2", "typescript": "^5.4.5", "vite-tsconfig-paths": "^4.3.2", "vitest": "^1.5.2" diff --git a/lib/r18gs/tsconfig.doc.json b/lib/r18gs/tsconfig.doc.json deleted file mode 100644 index 70249fd8..00000000 --- a/lib/r18gs/tsconfig.doc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "tsconfig/react-library.json", - "include": ["src"], - "exclude": ["**/*.test.tsx", "**/index.ts"] -} diff --git a/lib/r18gs/typedoc.config.js b/lib/r18gs/typedoc.config.js index dec526f7..aa844bf8 100644 --- a/lib/r18gs/typedoc.config.js +++ b/lib/r18gs/typedoc.config.js @@ -2,8 +2,15 @@ module.exports = { name: "Code Documentation", entryPoints: ["./src"], + exclude: ["**/*.test.tsx"], entryPointStrategy: "Expand", - tsconfig: "./tsconfig.doc.json", out: "../../docs", commentStyle: "all", + searchInComments: true, + plugin: [ + "typedoc-plugin-mdn-links", + "typedoc-plugin-rename-defaults", + "typedoc-plugin-missing-exports", + "typedoc-plugin-zod", + ], }; From 36ca5f52e336e2fa72cc82a8aa6df2b91de4d109 Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Tue, 30 Apr 2024 16:09:18 +0530 Subject: [PATCH 11/17] Update docs --- README.md | 80 +++--------------- lib/r18gs/index.ts | 5 -- lib/r18gs/server.ts | 3 - md-docs/getting-started.md | 84 +++++++++++++++++++ md-docs/using-plugins.md | 11 +++ .../shared-ui/hooks/use-persistant-counter.ts | 6 +- .../shared-ui/src/root/persistant-counter.tsx | 4 +- 7 files changed, 109 insertions(+), 84 deletions(-) delete mode 100644 lib/r18gs/index.ts delete mode 100644 lib/r18gs/server.ts create mode 100644 md-docs/getting-started.md create mode 100644 md-docs/using-plugins.md diff --git a/README.md b/README.md index 30f775aa..9590e6de 100644 --- a/README.md +++ b/README.md @@ -20,89 +20,29 @@ Thus, I decided to create a bare minimum, ultra-light store that creates shared ✅ Next.js, Vite and Remix examples -## Install +## Simple global state shared across multiple components -> A canonical package with longer name is also published `react18-global-store` - -```bash -$ pnpm add r18gs -``` - -or - -```bash -$ npm install r18gs -``` - -or - -```bash -$ yarn add r18gs -``` - -## Usage - -Use this hook similar to `useState` hook. - -The difference is that you need to pass an unique key - unique across the app to identify -and make this state accessible to all client components. +Utilize this hook similarly to the `useState` hook. However, ensure to pass a unique key, unique across the app, to identify and make this state accessible to all client components. ```tsx const [state, setState] = useRGS("counter", 1); ``` -You can access the same state across all client side components using unique key. +> For detailed instructions see [Getting Started](./md-docs/getting-started.md) -> It is recommended to store your keys in separate file to avoid typos and unnecessary conflicts. +## Using Plugins -### Example +You can enhance the functionality of the store by utilizing either the `create` function or the `useRGSWithPlugins` hook from `r18gs/dist/with-plugins`, enabling features such as storing to local storage, among others. ```tsx -// constants/global-states.ts -export const COUNTER = "counter"; -``` +// store.ts +import { create } from "r18gs/dist/with-plugins"; +import { persist } from "r18gs/dist/plugins"; /** You can create your own plugin or import third party plugin */ -```tsx -// components/display.tsx -"use client"; - -import useRGS from "r18gs"; -import { COUNTER } from "../constants/global-states"; - -export default function Display() { - const [count] = useRGS(COUNTER); - return ( -
-

Client component 2

- {count} -
- ); -} +export const useMyPersistentCounterStore = create("persistent-counter", 0, [persist()]); ``` -```tsx -// components/counter.tsx -"use client"; - -import useRGS from "r18gs"; -import { COUNTER } from "../constants/global-states"; - -export default function Counter() { - const [count, setCount] = useRGS(COUNTER, 0); - return ( -
-

Clinet component 1

- { - setCount(parseInt(e.target.value.trim())); - }} - type="number" - value={count} - /> -
- ); -} -``` +Now you can use `useMyPersistentCounterStore` similar to `useState` without any initial value. ## Contributing diff --git a/lib/r18gs/index.ts b/lib/r18gs/index.ts deleted file mode 100644 index e5de85df..00000000 --- a/lib/r18gs/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import useRGS from "./src"; - -export default useRGS; - -export * from "./src"; diff --git a/lib/r18gs/server.ts b/lib/r18gs/server.ts deleted file mode 100644 index dcc241ce..00000000 --- a/lib/r18gs/server.ts +++ /dev/null @@ -1,3 +0,0 @@ -/** to make sure import statements remain same for monorepo setup and install via npm */ - -export * from "./src/server"; diff --git a/md-docs/getting-started.md b/md-docs/getting-started.md new file mode 100644 index 00000000..e1d28c0d --- /dev/null +++ b/md-docs/getting-started.md @@ -0,0 +1,84 @@ +# Quick Start + +Welcome to the quick guide for using this library. + +## Installation + +To get started, you can install the package via your preferred package manager. Here are a few examples: + +```bash +$ pnpm add r18gs +``` + +or + +```bash +$ npm install r18gs +``` + +or + +```bash +$ yarn add r18gs +``` + +## Usage + +Utilize this hook similarly to the `useState` hook. However, ensure to pass a unique key, unique across the app, to identify and make this state accessible to all client components. + +```tsx +const [state, setState] = useRGS("counter", 1); +``` + +You can access the same state across all client-side components using a unique key. + +> It's advisable to store your keys in a separate file to prevent typos and unnecessary conflicts. + +### Example + +```tsx +// constants/global-states.ts +export const COUNTER = "counter"; +``` + +```tsx +// components/display.tsx +"use client"; + +import useRGS from "r18gs"; +import { COUNTER } from "../constants/global-states"; + +export default function Display() { + const [count] = useRGS(COUNTER); + return ( +
+

Client Component 2

+ {count} +
+ ); +} +``` + +```tsx +// components/counter.tsx +"use client"; + +import useRGS from "r18gs"; +import { COUNTER } from "../constants/global-states"; + +export default function Counter() { + const [count, setCount] = useRGS(COUNTER, 0); + return ( +
+

Client Component 1

+ { + setCount(parseInt(e.target.value.trim())); + }} + type="number" + value={count} + /> +
+ ); +} +``` diff --git a/md-docs/using-plugins.md b/md-docs/using-plugins.md new file mode 100644 index 00000000..a87977de --- /dev/null +++ b/md-docs/using-plugins.md @@ -0,0 +1,11 @@ +You can enhance the functionality of the store by utilizing either the `create` function or the `useRGSWithPlugins` hook from `r18gs/dist/with-plugins`, enabling features such as storing to local storage, among others. + +```tsx +// store.ts +import { create } from "r18gs/dist/with-plugins"; +import { persist } from "r18gs/dist/plugins"; /** You can create your own plugin or import third party plugin */ + +export const useMyPersistentCounterStore = create("persistent-counter", 0, [persist()]); +``` + +Now you can use `useMyPersistentCounterStore` similar to `useState` without any initial value. diff --git a/packages/shared-ui/hooks/use-persistant-counter.ts b/packages/shared-ui/hooks/use-persistant-counter.ts index 5852f566..18fdf006 100644 --- a/packages/shared-ui/hooks/use-persistant-counter.ts +++ b/packages/shared-ui/hooks/use-persistant-counter.ts @@ -1,6 +1,4 @@ import { create } from "r18gs/dist/with-plugins"; -import persistNSyncPlugin from "r18gs/dist/plugins/persist-and-sync"; +import { persist } from "r18gs/dist/plugins"; -export const useMyPersistantCounterStore = create("persistant-counter", 1, 1, [ - persistNSyncPlugin(), -]); +export const useMyPersistentCounterStore = create("persistent-counter", 1, [persist()]); diff --git a/packages/shared-ui/src/root/persistant-counter.tsx b/packages/shared-ui/src/root/persistant-counter.tsx index 48ea33c6..a504ab6a 100644 --- a/packages/shared-ui/src/root/persistant-counter.tsx +++ b/packages/shared-ui/src/root/persistant-counter.tsx @@ -1,7 +1,7 @@ "use client"; import { ChangeEvent, useCallback } from "react"; -import { useMyPersistantCounterStore } from "../../hooks/use-persistant-counter"; +import { useMyPersistentCounterStore } from "../../hooks/use-persistant-counter"; /** * Persistant Counter @@ -9,7 +9,7 @@ import { useMyPersistantCounterStore } from "../../hooks/use-persistant-counter" * @returns {JSX.Element} */ export default function PersistantCounter() { - const [count, setCount] = useMyPersistantCounterStore(); + const [count, setCount] = useMyPersistentCounterStore(); const handleChange = useCallback( (e: ChangeEvent) => { From fccc9fec12ffc8baf2a8b95c61a4bb062eeb79d9 Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Tue, 30 Apr 2024 18:45:47 +0530 Subject: [PATCH 12/17] Init with plugins --- lib/r18gs/src/utils.ts | 47 ++++++++++++++++++++++++++-- lib/r18gs/src/with-plugins.ts | 58 +++++++++++++++-------------------- 2 files changed, 69 insertions(+), 36 deletions(-) diff --git a/lib/r18gs/src/utils.ts b/lib/r18gs/src/utils.ts index 1371d86b..ede2ecae 100644 --- a/lib/r18gs/src/utils.ts +++ b/lib/r18gs/src/utils.ts @@ -10,7 +10,7 @@ export type SetStateAction = (value: SetterArgType) => void; * This is a hack to reduce lib size + readability + not encouraging direct access to globalThis */ const [VALUE, LISTENERS, SETTER, SUBSCRIBER] = [0, 1, 2, 3, 4]; -type RGS = [unknown, Listener[], SetStateAction, Subscriber]; +type RGS = [unknown, Listener[], SetStateAction | null, Subscriber]; declare global { // eslint-disable-next-line no-var -- var required for global declaration. @@ -77,7 +77,17 @@ async function initPlugins(key: string, plugins: Plugin[]) { } /** Initialize the named store when invoked for the first time. */ -export function initWithPlugins(key: string, value?: T, plugins: Plugin[] = []) { +export function initWithPlugins( + key: string, + value?: T, + plugins: Plugin[] = [], + doNotInit = false, +) { + if (doNotInit) { + /** You will not have access to the setter until initialized */ + globalRGS[key] = [value, [], null, createSubcriber(key)]; + return; + } /** setter function to set the state. */ const setterWithPlugins: SetStateAction = val => { /** Do not allow mutating the store before all extentions are initialized */ @@ -88,6 +98,37 @@ export function initWithPlugins(key: string, value?: T, plugins: Plugin[] plugins.forEach(plugin => plugin.onChange?.(key, rgs[VALUE] as T)); }; - globalRGS[key] = [value, [], setterWithPlugins, createSubcriber(key)]; + const rgs = globalRGS[key]; + if (rgs) { + rgs[VALUE] = value; + rgs[SETTER] = setterWithPlugins; + } else globalRGS[key] = [value, [], setterWithPlugins, createSubcriber(key)]; initPlugins(key, plugins); } + +/** + * Use this hook similar to `useState` hook. + * The difference is that you need to pass a + * unique key - unique across the app to make + * this state accessible to all client components. + * + * @example + * ```tsx + * const [state, setState] = useRGS("counter", 1); + * ``` + * + * @param key - Unique key to identify the store. + * @param value - Initial value of the store. + * @param plugins - Plugins to be applied to the store. + * @param doNotInit - Do not initialize the store. Useful when you want to initialize the store later. Note that the setter function is not available until the store is initialized. + * @returns - A tuple (Ordered sequance of values) containing the state and a function to set the state. + */ +export function useRGSWithPlugins( + key: string, + value?: T, + plugins?: Plugin[], + doNotInit = false, +): [T, SetStateAction] { + if (!globalRGS[key]?.[SETTER]) initWithPlugins(key, value, plugins, doNotInit); + return createHook(key); +} diff --git a/lib/r18gs/src/with-plugins.ts b/lib/r18gs/src/with-plugins.ts index 6f0bc232..b1825dde 100644 --- a/lib/r18gs/src/with-plugins.ts +++ b/lib/r18gs/src/with-plugins.ts @@ -1,46 +1,18 @@ /* eslint-disable @typescript-eslint/non-nullable-type-assertion-style -- as ! operator is forbidden by eslint*/ -import { createHook, globalRGS, initWithPlugins } from "./utils"; +import { useRGSWithPlugins } from "./utils"; import type { Plugin, SetStateAction } from "./utils"; /** - * Use this hook similar to `useState` hook. - * The difference is that you need to pass a - * unique key - unique across the app to make - * this state accessible to all client components. - * + * Creates a store with plugins. * @example - * ```tsx - * const [state, setState] = useRGS("counter", 1); - * ``` * - * @param key - Unique key to identify the store. - * @param value - Initial value of the store. - * @param plugins - Plugins to be applied to the store. - * @returns - A tuple (Ordered sequance of values) containing the state and a function to set the state. - */ -export function useRGSWithPlugins( - key: string, - value?: T, - plugins?: Plugin[], -): [T, SetStateAction] { - if (!globalRGS[key]) initWithPlugins(key, value, plugins); - return createHook(key); -} - -/** - * Use this hook similar to `useState` hook. - * The difference is that you need to pass a - * unique key - unique across the app to make - * this state accessible to all client components. - * - * @example * ```tsx - * // in hook file - * export const useRGS = create(key, value, plugins); + * // in hook file, e.g., store.ts + * export const useMyRGS = create(key, value, plugins); * * // in component file - * const [state, setState] = useRGS(); + * const [state, setState] = useMyRGS(); * ``` * * @param key - Unique key to identify the store. @@ -55,3 +27,23 @@ export function create( ): () => [T, SetStateAction] { return () => useRGSWithPlugins(key, value, plugins); } + +/** + * Creates a hook similar to useRGS, but with plugins to be applied on first invocation. + * + * @param plugins - Plugins to be applied to the store. + * @returns A hook that automatically initializes the store (if not already initialized) with the given plugins. + */ +export function withPlugins( + plugins?: Plugin[], +): (key: string, value?: T) => [T, SetStateAction] { + /** + * @param key - Unique key to identify the store. + * @param value - Initial value of the store. + * @param doNotInit - @defaultValue false - Do not initialize the store. Useful when you want to initialize the store later. Note that the setter function is not available until the store is initialized. + */ + return (key: string, value?: T, doNotInit = false) => + useRGSWithPlugins(key, value, plugins, doNotInit); +} + +export { useRGSWithPlugins }; From e94a29624ed7eea2d35919b0aaac87260725eeda Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Tue, 30 Apr 2024 18:53:59 +0530 Subject: [PATCH 13/17] Update docs --- README.md | 32 ++++++---- ...etting-started.md => 1.getting-started.md} | 0 md-docs/2.leveraging-plugins.md | 64 +++++++++++++++++++ md-docs/3.creating-plugins.md | 1 + md-docs/using-plugins.md | 11 ---- 5 files changed, 84 insertions(+), 24 deletions(-) rename md-docs/{getting-started.md => 1.getting-started.md} (100%) create mode 100644 md-docs/2.leveraging-plugins.md create mode 100644 md-docs/3.creating-plugins.md delete mode 100644 md-docs/using-plugins.md diff --git a/README.md b/README.md index 9590e6de..082bdb44 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ ## Motivation -I have built wonderful libraries utilizing React18 features using Zustand. They worked awesome. However, when I try importing from specific folder for better tree-shaking, the libraries fail. This is because, for each import a separate zustand store is created. This actually increases the package size also. +I've developed fantastic libraries leveraging React18 features using Zustand, and they performed admirably. However, when attempting to import from specific folders for better tree-shaking, the libraries encountered issues. Each import resulted in a separate Zustand store being created, leading to increased package size. -Thus, I decided to create a bare minimum, ultra-light store that creates shared state even while importing components from separate files for better treeshaking. +As a solution, I set out to create a lightweight, bare minimum store that facilitates shared state even when importing components from separate files, optimizing tree-shaking. ## Features @@ -14,13 +14,13 @@ Thus, I decided to create a bare minimum, ultra-light store that creates shared ✅ Unleash the full power of React18 Server components -✅ Works with all build systems/tools/frameworks for React18 +✅ Compatible with all build systems/tools/frameworks for React18 -✅ Doccumented with [Typedoc](https://react18-tools.github.io/react18-global-store) ([Docs](https://react18-tools.github.io/react18-global-store)) +✅ Documented with [Typedoc](https://react18-tools.github.io/react18-global-store) ([Docs](https://react18-tools.github.io/react18-global-store)) -✅ Next.js, Vite and Remix examples +✅ Examples for Next.js, Vite, and Remix -## Simple global state shared across multiple components +## Simple Global State Shared Across Multiple Components Utilize this hook similarly to the `useState` hook. However, ensure to pass a unique key, unique across the app, to identify and make this state accessible to all client components. @@ -28,35 +28,41 @@ Utilize this hook similarly to the `useState` hook. However, ensure to pass a un const [state, setState] = useRGS("counter", 1); ``` -> For detailed instructions see [Getting Started](./md-docs/getting-started.md) +> For detailed instructions, see [Getting Started](./md-docs/1.getting-started.md) ## Using Plugins -You can enhance the functionality of the store by utilizing either the `create` function or the `useRGSWithPlugins` hook from `r18gs/dist/with-plugins`, enabling features such as storing to local storage, among others. +Enhance the functionality of the store by leveraging either the `create` function, `withPlugins` function, or the `useRGSWithPlugins` hook from `r18gs/dist/with-plugins`, enabling features such as storing to local storage, among others. ```tsx // store.ts import { create } from "r18gs/dist/with-plugins"; -import { persist } from "r18gs/dist/plugins"; /** You can create your own plugin or import third party plugin */ +import { persist } from "r18gs/dist/plugins"; /** You can create your own plugin or import third-party plugins */ export const useMyPersistentCounterStore = create("persistent-counter", 0, [persist()]); ``` -Now you can use `useMyPersistentCounterStore` similar to `useState` without any initial value. +Now, you can utilize `useMyPersistentCounterStore` similarly to `useState` without specifying an initial value. + +```tsx +const [persistedCount, setPersistedCount] = useMyPersistentCounterStore(); +``` + +> For detailed instructions, see [Leveraging Plugins](./md-docs/2.leveraging-plugins.md) ## Contributing See [contributing.md](/contributing.md) -### 🤩 Don't forger to star [this repo](https://github.com/mayank1513/react18-global-store)! +### 🤩 Don't forget to star [this repo](https://github.com/mayank1513/react18-global-store)! -Want hands-on course for getting started with Turborepo? Check out [React and Next.js with TypeScript](https://mayank-chaudhari.vercel.app/courses/react-and-next-js-with-typescript) and [The Game of Chess with Next.js, React and TypeScrypt](https://www.udemy.com/course/game-of-chess-with-nextjs-react-and-typescrypt/?referralCode=851A28F10B254A8523FE) +Interested in hands-on courses for getting started with Turborepo? Check out [React and Next.js with TypeScript](https://mayank-chaudhari.vercel.app/courses/react-and-next-js-with-typescript) and [The Game of Chess with Next.js, React and TypeScript](https://www.udemy.com/course/game-of-chess-with-nextjs-react-and-typescript/?referralCode=851A28F10B254A8523FE) ![Repo Stats](https://repobeats.axiom.co/api/embed/ec3e74d795ed805a0fce67c0b64c3f08872e7945.svg "Repobeats analytics image") ## License -Licensed as MIT open source. +Licensed under the MPL 2.0 open source license.
diff --git a/md-docs/getting-started.md b/md-docs/1.getting-started.md similarity index 100% rename from md-docs/getting-started.md rename to md-docs/1.getting-started.md diff --git a/md-docs/2.leveraging-plugins.md b/md-docs/2.leveraging-plugins.md new file mode 100644 index 00000000..e8bd53c2 --- /dev/null +++ b/md-docs/2.leveraging-plugins.md @@ -0,0 +1,64 @@ +# Leveraging Plugins + +Enhance your store's functionality by utilizing either the `create` function, `withPlugins` function, or the `useRGSWithPlugins` hook from `r18gs/dist/with-plugins`. This enables features such as storing to local storage, among others. + +## Creating a Store with the `create` Function + +```tsx +// store.ts +import { create } from "r18gs/dist/with-plugins"; +import { persist } from "r18gs/dist/plugins"; /** You can create your own plugin or import third-party plugins */ + +export const useMyPersistentCounterStore = create("persistent-counter", 0, [persist()]); +``` + +Now you can utilize `useMyPersistentCounterStore` similar to `useState` without specifying an initial value. + +```tsx +// inside your component +const [persistedCount, setPersistedCount] = useMyPersistentCounterStore(); +``` + +## Utilizing the `useRGSWithPlugins` Hook + +This function is beneficial if your requirements dictate that your `key` and/or initial value will depend on some props, etc. Or for some other reason you want to initialize the store from within a component (You need to use some variables available inside the component). + +```tsx +import { useRGSWithPlugins } from "r18gs/dist/with-plugin"; + +export function MyComponent(props: MyComponentProps) { + const [state, setState] = useRGSWithPlugins( + props.key, + props.initialVal, + props.plugins, + props.doNotInit, + ); + // ... +} +``` + +### `doNotInit` + +In some cases, you may not want to initialize the store immediately. In such cases, you can pass the fourth argument (`doNotInit`) as `true`. The default value is false. + +- When this argument is set to `true`, the store is created, but the setter function is set to null. Thus, you can access the initial value set by the first component that triggered this hook. However, it cannot be modified until the store is initialized, i.e., the hook is invoked by a component setting `doNotInit` to false (or skipping this argument). + +**Use case**: When you need that the store be used in many components, however, it should be initialized in a particular component only. + +## Creating a Custom Hook using the `withPlugins` Higher-Order Function + +This is a utility function that will be very helpful when you want to use the `useRGSWithPlugins` hook with the same plugins in multiple components. + +```ts +// custom hook file, e.g., store.ts +export const useMyRGS = withPlugins([plugin1, plugin2, ...]); +``` + +```tsx +export function MyComponent(props: MyComponentProps) { + const [state, setState] = useMyRGS(props.key, props.initialVal, props.doNotInit); + // ... +} +``` + +You can also create your own plugins. Refer to [Creating Plugins](./3.creating-plugins.md). diff --git a/md-docs/3.creating-plugins.md b/md-docs/3.creating-plugins.md new file mode 100644 index 00000000..b7d3cbd8 --- /dev/null +++ b/md-docs/3.creating-plugins.md @@ -0,0 +1 @@ +# Creating Plugins diff --git a/md-docs/using-plugins.md b/md-docs/using-plugins.md deleted file mode 100644 index a87977de..00000000 --- a/md-docs/using-plugins.md +++ /dev/null @@ -1,11 +0,0 @@ -You can enhance the functionality of the store by utilizing either the `create` function or the `useRGSWithPlugins` hook from `r18gs/dist/with-plugins`, enabling features such as storing to local storage, among others. - -```tsx -// store.ts -import { create } from "r18gs/dist/with-plugins"; -import { persist } from "r18gs/dist/plugins"; /** You can create your own plugin or import third party plugin */ - -export const useMyPersistentCounterStore = create("persistent-counter", 0, [persist()]); -``` - -Now you can use `useMyPersistentCounterStore` similar to `useState` without any initial value. From 5114473e80eb0f5243e7a2f006e44e7270d21670 Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Tue, 30 Apr 2024 18:54:23 +0530 Subject: [PATCH 14/17] Remove license file --- LICENSE | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 LICENSE diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 78ecd9da..00000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2023 Mayank - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. From d2466e990047676ae8319758ea6110c6395188d0 Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Tue, 30 Apr 2024 18:58:03 +0530 Subject: [PATCH 15/17] Update license field --- lib/r18gs/package.json | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/r18gs/package.json b/lib/r18gs/package.json index 8f2037e7..0c01c1ed 100644 --- a/lib/r18gs/package.json +++ b/lib/r18gs/package.json @@ -19,7 +19,7 @@ }, "homepage": "https://react18-global-store.vercel.app/", "sideEffects": false, - "license": "MIT", + "license": "MPL-2.0", "scripts": { "test": "vitest run --coverage", "build": "tsup src && tsc -p tsconfig-build.json", @@ -54,10 +54,13 @@ "@types/react": "16.8 - 18", "react": "16.8 - 18" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/mayank1513" - }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mayank1513" + }, + "https://pages.razorpay.com/mayank1513" + ], "keywords": [ "react18-global-store", "nextjs", From 206ffe462e83e4e8ddb5d775c36453de9b647852 Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Tue, 30 Apr 2024 19:27:39 +0530 Subject: [PATCH 16/17] Update tests --- lib/r18gs/src/with-plugins.ts | 3 +- lib/r18gs/tests/create.test.tsx | 38 +++++++++++++++++++++--- lib/r18gs/tests/plugins/persist.test.tsx | 2 -- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/lib/r18gs/src/with-plugins.ts b/lib/r18gs/src/with-plugins.ts index b1825dde..d877ce43 100644 --- a/lib/r18gs/src/with-plugins.ts +++ b/lib/r18gs/src/with-plugins.ts @@ -36,8 +36,9 @@ export function create( */ export function withPlugins( plugins?: Plugin[], -): (key: string, value?: T) => [T, SetStateAction] { +): (key: string, value?: T, doNotInit?: boolean) => [T, SetStateAction] { /** + * todo - this typedoc comments are not visible in IDE suggestions - fix this * @param key - Unique key to identify the store. * @param value - Initial value of the store. * @param doNotInit - @defaultValue false - Do not initialize the store. Useful when you want to initialize the store later. Note that the setter function is not available until the store is initialized. diff --git a/lib/r18gs/tests/create.test.tsx b/lib/r18gs/tests/create.test.tsx index 5d1ab45c..f5f1e09f 100644 --- a/lib/r18gs/tests/create.test.tsx +++ b/lib/r18gs/tests/create.test.tsx @@ -1,39 +1,59 @@ import { describe, test } from "vitest"; import { act, fireEvent, render, screen } from "@testing-library/react"; -import { create } from "../src/with-plugins"; +import { create, withPlugins } from "../src/with-plugins"; import { persist } from "../src/plugins/persist"; import { ChangeEvent, useCallback } from "react"; -const COUNTER_RGS_KEY = "count"; +const COUNTER_RGS_KEY = "counter"; const TESTID_INPUT = "in1"; const TESTID_DISPLAY = "d1"; +const COUNTER2_RGS_KEY = "counter-2"; +const TESTID_INPUT2 = "in2"; +const TESTID_DISPLAY2 = "d2"; + const useMyRGS = create(COUNTER_RGS_KEY, 0, [persist({ storage: "cookie" })]); +const useMyRGS2 = withPlugins([persist()]); + function Component1() { const [count, setCount] = useMyRGS(); + const [count2, setCount2] = useMyRGS2(COUNTER2_RGS_KEY, 0); const handleChange = useCallback( (e: ChangeEvent) => { setCount(parseInt(e.target.value)); }, [setCount], ); + const handleChange2 = useCallback( + (e: ChangeEvent) => { + setCount2(parseInt(e.target.value)); + }, + [setCount2], + ); return (
+
); } function Component2() { const [count] = useMyRGS(); - return

{count}

; + const [count2] = useMyRGS2(COUNTER2_RGS_KEY, 10, true); + return ( + <> +

{count}

+

{count2}

+ + ); } describe("React18GlobalStore", () => { test("check state update to multiple components", async ({ expect }) => { - render(); render(); + render(); /** Await and allow for the state to update from localStorate */ await new Promise(resolve => setTimeout(resolve, 100)); await act(() => fireEvent.input(screen.getByTestId(TESTID_INPUT), { target: { value: 5 } })); @@ -41,6 +61,16 @@ describe("React18GlobalStore", () => { expect(JSON.parse(sessionStorage.getItem(COUNTER_RGS_KEY) ?? "{}").val).toBe(5); }); + test("check state update to multiple components for withPlugins", async ({ expect }) => { + render(); + render(); + /** Await and allow for the state to update from localStorate */ + await new Promise(resolve => setTimeout(resolve, 100)); + await act(() => fireEvent.input(screen.getByTestId(TESTID_INPUT2), { target: { value: 5 } })); + expect(screen.getByTestId(TESTID_DISPLAY2).textContent).toBe("5"); + expect(JSON.parse(localStorage.getItem(COUNTER2_RGS_KEY) ?? "{}").val).toBe(5); + }); + test("storage event", async ({ expect }) => { render(); render(); diff --git a/lib/r18gs/tests/plugins/persist.test.tsx b/lib/r18gs/tests/plugins/persist.test.tsx index 57d2f2de..3fceadb5 100644 --- a/lib/r18gs/tests/plugins/persist.test.tsx +++ b/lib/r18gs/tests/plugins/persist.test.tsx @@ -49,8 +49,6 @@ describe("React18GlobalStore", () => { await new Promise(resolve => setTimeout(resolve, 100)); await act(() => fireEvent.input(screen.getByTestId(TESTID_INPUT), { target: { value: 5 } })); expect(screen.getByTestId(TESTID_DISPLAY).textContent).toBe("5"); - console.log("sessionStorage --- ", sessionStorage.getItem(COUNTER_RGS_KEY), options); - console.log("localStorage --- ", localStorage.getItem(COUNTER_RGS_KEY)); expect( JSON.parse( ((options.storage ?? "local") === "local" ? localStorage : sessionStorage).getItem( From 7b71f71f1044cea39d325b0d20bdecf88b22fec6 Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Tue, 30 Apr 2024 20:04:32 +0530 Subject: [PATCH 17/17] Fix types --- lib/r18gs/src/with-plugins.ts | 22 +++++++++++++++------- lib/r18gs/tests/create.test.tsx | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/r18gs/src/with-plugins.ts b/lib/r18gs/src/with-plugins.ts index d877ce43..70a2db46 100644 --- a/lib/r18gs/src/with-plugins.ts +++ b/lib/r18gs/src/with-plugins.ts @@ -18,7 +18,7 @@ import type { Plugin, SetStateAction } from "./utils"; * @param key - Unique key to identify the store. * @param value - Initial value of the store. * @param plugins - Plugins to be applied to the store. - * @returns - A hook funciton that returns a tuple (Ordered sequance of values) containing the state and a function to set the state. + * @returns - A hook function that returns a tuple (Ordered sequence of values) containing the state and a function to set the state. */ export function create( key: string, @@ -35,16 +35,24 @@ export function create( * @returns A hook that automatically initializes the store (if not already initialized) with the given plugins. */ export function withPlugins( - plugins?: Plugin[], -): (key: string, value?: T, doNotInit?: boolean) => [T, SetStateAction] { + plugins: Plugin[], +): (key: string, value?: U, doNotInit?: boolean) => [U, SetStateAction] { /** - * todo - this typedoc comments are not visible in IDE suggestions - fix this + * Creates a hook similar to useRGS, with plugins applied on first invocation. + * * @param key - Unique key to identify the store. * @param value - Initial value of the store. - * @param doNotInit - @defaultValue false - Do not initialize the store. Useful when you want to initialize the store later. Note that the setter function is not available until the store is initialized. + * @param doNotInit - If true, the store won't be initialized immediately. Defaults to false. Useful when you want to initialize the store later. Note that the setter function is not available until the store is initialized. + * @returns A tuple containing the state value and its setter function. */ - return (key: string, value?: T, doNotInit = false) => - useRGSWithPlugins(key, value, plugins, doNotInit); + const hookWithPlugins = ( + key: string, + value?: U, + doNotInit = false, + ): [U, SetStateAction] => + useRGSWithPlugins(key, value, plugins as unknown as Plugin[], doNotInit); + + return hookWithPlugins; } export { useRGSWithPlugins }; diff --git a/lib/r18gs/tests/create.test.tsx b/lib/r18gs/tests/create.test.tsx index f5f1e09f..cfa5d77e 100644 --- a/lib/r18gs/tests/create.test.tsx +++ b/lib/r18gs/tests/create.test.tsx @@ -14,7 +14,7 @@ const TESTID_DISPLAY2 = "d2"; const useMyRGS = create(COUNTER_RGS_KEY, 0, [persist({ storage: "cookie" })]); -const useMyRGS2 = withPlugins([persist()]); +const useMyRGS2 = withPlugins([persist()]); function Component1() { const [count, setCount] = useMyRGS();