diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5b408f87..8c2a6b39 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,11 +13,11 @@ jobs: packages: write contents: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: registry-url: https://registry.npmjs.org node-version: 20 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 80bcefbd..d6c6947f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,11 +22,11 @@ jobs: working-directory: ./lib/r18gs steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 20 registry-url: https://registry.npmjs.org @@ -35,30 +35,26 @@ jobs: # fail and not publish if any of the unit tests are failing - name: Test run: pnpm test - - name: Create release and publish to NPM + - name: publish to NPM run: pnpm build && pnpm publish-package - # continue on error to publish scoped package name <- by default repo is setup for a non-scoped + scoped package name - continue-on-error: true env: NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} TOKEN: ${{ secrets.GITHUB_TOKEN }} OWNER: ${{ github.event.repository.owner.login }} REPO: ${{ github.event.repository.name }} - - name: Publish Scoped package to NPM - # continue on error - expecing npm to trow error if scoping is done twice - continue-on-error: true - run: cd dist && node ../scope.js && npm publish --provenance --access public && cd .. + - name: Create GitHub release + run: | + v=$(node.exe -p -e "require('./package.json').version") + gh release create $v --generate-notes --latest -F CHANGELOG.md --title "Release $v" env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} - TOKEN: ${{ secrets.GITHUB_TOKEN }} - OWNER: ${{ github.event.repository.owner.login }} - REPO: ${{ github.event.repository.name }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Publish package with long name for better SEO + - name: Publish canonical packages continue-on-error: true run: | - cd dist + sed -i -e "s/.*name.*/\t\"name\": \"@mayank1513\/r18gs\",/" package.json + npm publish --provenance --access public sed -i -e "s/.*name.*/\t\"name\": \"react18-global-store\",/" package.json npm publish --provenance --access public sed -i -e "s/.*name.*/\t\"name\": \"react18-store\",/" package.json diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c83f11f1..15e4a2ba 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,8 +11,8 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 20 - run: npm i -g pnpm && pnpm i @@ -21,7 +21,7 @@ jobs: run: pnpm test - name: Upload coverage reports to Codecov continue-on-error: true - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: directory: ./lib/r18gs token: ${{ secrets.CODECOV_TOKEN }} diff --git a/README.md b/README.md index 6c6724c8..30f775aa 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,6 @@ Thus, I decided to create a bare minimum, ultra-light store that creates shared ✅ Works with all build systems/tools/frameworks for React18 -✅ Works for both client side and server side components (be careful, separate states are created for server side and client side. Any changes done on client side will not affect the server components.) - ✅ Doccumented 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 @@ -53,7 +51,7 @@ and make this state accessible to all client components. const [state, setState] = useRGS("counter", 1); ``` -You can access the same state across all client side components using unique. +You can access the same state across all client side components using unique key. > It is recommended to store your keys in separate file to avoid typos and unnecessary conflicts. @@ -106,30 +104,9 @@ export default function Counter() { } ``` -## Contribute - -### Build - -To build all apps and packages, run the following command: - -``` -cd react18-global-store -pnpm build -``` - -### Develop - -To develop all apps and packages, run the following command: - -``` -cd react18-global-store -pnpm dev -``` - -Also, please +## Contributing -1. check out discussion for providing any feedback or sugestions. -2. Report any issues or feature requests in issues tab +See [contributing.md](/contributing.md) ### 🤩 Don't forger to star [this repo](https://github.com/mayank1513/react18-global-store)! diff --git a/contributing.md b/contributing.md index 366c01b6..87045151 100644 --- a/contributing.md +++ b/contributing.md @@ -1,41 +1,14 @@ -# Contributing to fork-me +# Contributing to r18gs -## What's inside? - -### Utilities - -This Turborepo has some additional tools already setup for you: +## Contribute -- [TypeScript](https://www.typescriptlang.org/) for static type checking -- [ESLint](https://eslint.org/) for code linting -- [Prettier](https://prettier.io) for code formatting - -### Apps and Packages - -This Turborepo includes the following packages/examples: - -- `nextjs`: a [Next.js](https://nextjs.org/) app -- `vite`: a [Vite.js](https://vitest.dev) app -- `eslint-config-custom`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`) -- `tsconfig`: `tsconfig.json`s used throughout the monorepo - -To increase the visibility we have moved core library to lib - -- `fork-me`: a React component library shared by both `nextjs` and `vite` examples lives in `./lib/fork-me` - -Each package/example is 100% [TypeScript](https://www.typescriptlang.org/). - -## Automatic file generation - -- just run `yarn turbo gen` and follow the propts to auto generate your new component with test file and dependency linking -- follow best practices automatically +Once you have cloned the repo. `cd` to the repo directory and use following commands. ### Build To build all apps and packages, run the following command: ```bash -cd r18gs pnpm build ``` @@ -44,7 +17,6 @@ pnpm build To develop all apps and packages, run the following command: ```bash -cd r18gs pnpm dev ``` @@ -53,7 +25,6 @@ pnpm dev To run unit tests, run the following command: ```bash -cd r18gs pnpm test ``` @@ -62,7 +33,6 @@ pnpm test Before creating PR make sure lint is passing and also run formatter to properly format the code. ```bash -cd r18gs pnpm lint ``` @@ -72,6 +42,49 @@ and pnpm format ``` +You can also contribute by + +1. Sponsoring +2. Check out discussion for providing any feedback or sugestions. +3. Report any issues or feature requests in issues tab + +## What's inside? + +### Utilities + +This Turborepo has some additional tools already setup for you: + +- [TypeScript](https://www.typescriptlang.org/) for static type checking +- [ESLint](https://eslint.org/) for code linting +- [Prettier](https://prettier.io) for code formatting + +### Library, Examples and Packages + +This Turborepo includes the following packages/examples: + +#### React18 Global Store library + +You will find the core library code inside lib/r18gs + +#### Examples (/examples) + +- `nextjs`: a [Next.js](https://nextjs.org/) app +- `vite`: a [Vite.js](https://vitest.dev) app +- `remix`: a [Remix](https://remix.run/) app + +#### Packages (/packages) + +- `eslint-config-custom`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`) +- `tsconfig`: `tsconfig.json`s used throughout the monorepo +- `shared-ui`: An internal UI package for shared UI code + +Each package/example is 100% [TypeScript](https://www.typescriptlang.org/). + +## Automatic file generation + +- just run `yarn turbo gen` and follow the propts to auto generate your new component with test file and dependency linking +- follow best practices automatically + ## Useful Links Learn more about Turborepo and Next.js: @@ -89,4 +102,6 @@ Learn more about Turborepo and Next.js:
+### 🤩 Don't forger to star [this repo](https://github.com/mayank1513/react18-global-store)! +

with 💖 by Mayank Kumar Chaudhari

diff --git a/examples/nextjs/CHANGELOG.md b/examples/nextjs/CHANGELOG.md index 73691cb9..20c63a52 100644 --- a/examples/nextjs/CHANGELOG.md +++ b/examples/nextjs/CHANGELOG.md @@ -1,5 +1,14 @@ # nextjs-example +## 0.0.11 + +### Patch Changes + +- Updated dependencies [81b9d3f] +- Updated dependencies + - r18gs@0.2.0 + - shared-ui@0.0.0 + ## 0.0.10 ### Patch Changes diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index 75c45be8..b92f5d3f 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "nextjs-example", - "version": "0.0.10", + "version": "0.0.11", "private": true, "scripts": { "dev": "next dev", diff --git a/examples/remix/CHANGELOG.md b/examples/remix/CHANGELOG.md index 2a2260ce..f9cd871d 100644 --- a/examples/remix/CHANGELOG.md +++ b/examples/remix/CHANGELOG.md @@ -1,5 +1,15 @@ # remix-example +## 0.0.11 + +### Patch Changes + +- 81b9d3f: Make the library extensible with plugins +- Updated dependencies [81b9d3f] +- Updated dependencies + - r18gs@0.2.0 + - shared-ui@0.0.0 + ## 0.0.10 ### Patch Changes diff --git a/examples/remix/package.json b/examples/remix/package.json index 96539b22..015a2b3c 100644 --- a/examples/remix/package.json +++ b/examples/remix/package.json @@ -1,6 +1,6 @@ { "name": "remix-example", - "version": "0.0.10", + "version": "0.0.11", "private": true, "sideEffects": false, "type": "module", @@ -33,19 +33,5 @@ }, "engines": { "node": ">=18.0.0" - }, - "pnpm": { - "packageExtensions": { - "@remix-run/dev": { - "peerDependencies": { - "react-dom": "18.2.0" - } - }, - "@remix-run/serve": { - "peerDependencies": { - "react-dom": "18.2.0" - } - } - } } } diff --git a/examples/vite/CHANGELOG.md b/examples/vite/CHANGELOG.md index c01b7cc1..0604d258 100644 --- a/examples/vite/CHANGELOG.md +++ b/examples/vite/CHANGELOG.md @@ -1,5 +1,14 @@ # vite-example +## 0.0.11 + +### Patch Changes + +- Updated dependencies [81b9d3f] +- Updated dependencies + - r18gs@0.2.0 + - shared-ui@0.0.0 + ## 0.0.10 ### Patch Changes diff --git a/examples/vite/package.json b/examples/vite/package.json index c3b4cf88..d1ce5450 100644 --- a/examples/vite/package.json +++ b/examples/vite/package.json @@ -1,7 +1,7 @@ { "name": "vite-example", "private": true, - "version": "0.0.10", + "version": "0.0.11", "type": "module", "scripts": { "dev": "vite --port 3001", diff --git a/lib/r18gs/CHANGELOG.md b/lib/r18gs/CHANGELOG.md index 170af46d..b0319ffa 100644 --- a/lib/r18gs/CHANGELOG.md +++ b/lib/r18gs/CHANGELOG.md @@ -1,5 +1,15 @@ # r18gs +## 0.2.0 + +### Minor Changes + +- 81b9d3f: Make the library extensible with plugins + +### Patch Changes + +- Add Persist and Sync plugin + ## 0.1.4 ### Patch Changes diff --git a/lib/r18gs/package.json b/lib/r18gs/package.json index 4860f34d..f579d2f8 100644 --- a/lib/r18gs/package.json +++ b/lib/r18gs/package.json @@ -2,10 +2,14 @@ "name": "r18gs", "author": "Mayank Kumar Chaudhari ", "private": false, - "version": "0.1.4", + "version": "0.2.0", "description": "A simple yet elegant, light weight, react18 global store to replace Zustand for better tree shaking.", - "main": "./index.ts", - "types": "./index.ts", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "module": "./dist/index.mjs", + "files": [ + "dist/**" + ], "repository": { "type": "git", "url": "git+https://github.com/react18-tools/react18-global-store.git" @@ -18,9 +22,9 @@ "license": "MIT", "scripts": { "test": "vitest run --coverage", - "build": "tsup src && tsc -p tsconfig-build.json && node touchup.js", + "build": "tsup src && tsc -p tsconfig-build.json", "doc": "cp ../../README.md . && typedoc", - "publish-package": "cd dist && npm publish --provenance --access public", + "publish-package": "cp ../../README.md . && npm publish --provenance --access public", "lint": "eslint ." }, "devDependencies": { diff --git a/lib/r18gs/scope.js b/lib/r18gs/scope.js deleted file mode 100644 index e9e42268..00000000 --- a/lib/r18gs/scope.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable -- no need - external file */ -"use strict"; - -const fs = require("node:fs"); -const path = require("node:path"); - -const owner = "mayank1513"; -const packageJson = require(path.resolve(process.cwd(), "package.json")); -const ref = packageJson.name; -if (!ref.startsWith(`@${owner}`)) { - packageJson.name = `@${owner}/${packageJson.name}`; - fs.writeFileSync( - path.resolve(process.cwd(), "package.json"), - JSON.stringify(packageJson, null, 2), - ); - const readMePath = path.resolve(process.cwd(), "README.md"); - let readMe = fs.readFileSync(readMePath, { encoding: "utf8" }); - const tmp = "!--|--!"; - readMe = readMe.replace(new RegExp(`$${owner}/${ref}`, "g"), tmp); - readMe = readMe.replace(new RegExp(ref, "g"), packageJson.name); - readMe = readMe.replace(new RegExp(tmp, "g"), `$${owner}/${ref}`); - fs.writeFileSync(readMePath, readMe); -} diff --git a/lib/r18gs/src/index.ts b/lib/r18gs/src/index.ts index 06fe8d72..3d70ae81 100644 --- a/lib/r18gs/src/index.ts +++ b/lib/r18gs/src/index.ts @@ -1,5 +1,4 @@ import useRGS from "./use-rgs"; +export type { SetterArgType, SetStateAction, Plugin } from "./utils"; export default useRGS; - -export * from "./use-rgs"; diff --git a/lib/r18gs/src/plugins/persist-and-sync.ts b/lib/r18gs/src/plugins/persist-and-sync.ts new file mode 100644 index 00000000..9d04c9cf --- /dev/null +++ b/lib/r18gs/src/plugins/persist-and-sync.ts @@ -0,0 +1,31 @@ +import { Plugin } from ".."; + +/** + * A plugin that persists and syncs RGS store between tabs. + * + * @returns A plugin that persists and syncs a value between tabs. + */ +function persistAndSyncPlugin(): Plugin { + return { + init(key, value, _, mutate) { + if (typeof window === "undefined") return; + const persistedValue = localStorage.getItem(key); + 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); + } + }); + }, + onChange(key, value) { + if (typeof window !== "undefined") { + localStorage.setItem(key, JSON.stringify({ val: value })); + } + }, + }; +} + +export default persistAndSyncPlugin; diff --git a/lib/r18gs/src/use-rgs.ts b/lib/r18gs/src/use-rgs.ts index 123d5c30..11070222 100644 --- a/lib/r18gs/src/use-rgs.ts +++ b/lib/r18gs/src/use-rgs.ts @@ -1,46 +1,7 @@ /* eslint-disable @typescript-eslint/non-nullable-type-assertion-style -- as ! operator is forbidden by eslint*/ -import { useSyncExternalStore } from "react"; +import { createHook, createSetter, createSubcriber, globalRGS } from "./utils"; -type Listener = () => void; -type Subscriber = (l: Listener) => () => void; -export type SetterArgType = T | ((prevState: T) => T); -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]; -type RGS = [unknown, Listener[], SetStateAction, Subscriber]; - -declare global { - // eslint-disable-next-line no-var -- var required for global declaration. - var rgs: Record; -} - -const globalThisForBetterMinification = globalThis; -globalThisForBetterMinification.rgs = {}; -const globalRGS = globalThisForBetterMinification.rgs; - -/** Initialize the named store when invoked for the first time. */ -function init(key: string, value?: T) { - const listeners: Listener[] = []; - /** setter function to set the state. */ - const setter: SetStateAction = val => { - const rgs = globalRGS[key] as RGS; - rgs[VALUE] = val instanceof Function ? val(rgs[VALUE] as T) : val; - (rgs[LISTENERS] as Listener[]).forEach(listener => listener()); - }; - /** subscriber function to subscribe to the store. */ - const subscriber: Subscriber = listener => { - const rgs = globalRGS[key] as RGS; - const listeners = rgs[LISTENERS] as Listener[]; - listeners.push(listener); - return () => { - rgs[LISTENERS] = listeners.filter(l => l !== listener); - }; - }; - globalRGS[key] = [value, listeners, setter as SetStateAction, subscriber]; -} +import type { SetStateAction } from "./utils"; /** * Use this hook similar to `useState` hook. @@ -59,17 +20,9 @@ function init(key: string, value?: T) { * @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] { - if (!globalRGS[key]) init(key, value); - - 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] ?? value) as T; - /** Function to get server snapshot. Returns server value is provided else the default value. */ - const getServerSnap = () => (serverValue ?? rgs[VALUE] ?? value) as T; + /** Initialize the named store when invoked for the first time. */ + if (!globalRGS[key]) + globalRGS[key] = [value, serverValue, [], createSetter(key), createSubcriber(key)]; - const val = useSyncExternalStore(rgs[SUBSCRIBER] as Subscriber, getSnap, getServerSnap); - return [val, setRGState]; + return createHook(key); } diff --git a/lib/r18gs/src/utils.ts b/lib/r18gs/src/utils.ts new file mode 100644 index 00000000..0adcd1a0 --- /dev/null +++ b/lib/r18gs/src/utils.ts @@ -0,0 +1,107 @@ +import { useSyncExternalStore } from "react"; + +type Listener = () => void; +type Subscriber = (l: Listener) => () => void; + +export type SetterArgType = T | ((prevState: T) => T); +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]; + +declare global { + // eslint-disable-next-line no-var -- var required for global declaration. + var rgs: Record; +} + +const globalThisForBetterMinification = globalThis; +globalThisForBetterMinification.rgs = {}; +export const globalRGS = globalThisForBetterMinification.rgs; + +/** trigger all listeners */ +function triggerListeners(rgs: RGS) { + (rgs[LISTENERS] as Listener[]).forEach(listener => listener()); +} + +/** craete subscriber function to subscribe to the store. */ +export function createSubcriber(key: string): Subscriber { + return listener => { + const rgs = globalRGS[key] as RGS; + const listeners = rgs[LISTENERS] as Listener[]; + listeners.push(listener); + return () => { + rgs[LISTENERS] = listeners.filter(l => l !== listener); + }; + }; +} + +/** setter function to set the state. */ +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); + }; +} + +/** 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; + /** 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); + return [val, setRGState]; +} + +type Mutate = (value?: T, serverValue?: 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; +}; + +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) => { + 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); + } + allExtentionsInitialized = true; +} + +/** Initialize the named store when invoked for the first time. */ +export function initWithPlugins( + key: string, + value?: T, + serverValue?: T, + plugins: Plugin[] = [], +) { + /** setter function to set the state. */ + const setterWithPlugins: SetStateAction = val => { + /** Do not allow mutating the store before all extentions are initialized */ + if (!allExtentionsInitialized) return; + 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)); + }; + + globalRGS[key] = [value, serverValue, [], setterWithPlugins, createSubcriber(key)]; + initPlugins(key, plugins); +} diff --git a/lib/r18gs/src/with-plugins.ts b/lib/r18gs/src/with-plugins.ts new file mode 100644 index 00000000..3a5042ff --- /dev/null +++ b/lib/r18gs/src/with-plugins.ts @@ -0,0 +1,61 @@ +/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style -- as ! operator is forbidden by eslint*/ +import { createHook, globalRGS, initWithPlugins } 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. + * + * @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. + * @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); + 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, serverValue, plugins); + * + * // in component file + * const [state, setState] = useRGS(); + * ``` + * + * @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); +} diff --git a/lib/r18gs/tests/create.test.tsx b/lib/r18gs/tests/create.test.tsx new file mode 100644 index 00000000..72615c0e --- /dev/null +++ b/lib/r18gs/tests/create.test.tsx @@ -0,0 +1,61 @@ +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 { ChangeEvent, useCallback } from "react"; + +const COUNTER_RGS_KEY = "count"; + +const useMyRGS = create(COUNTER_RGS_KEY, 0, 0, [persistAndSyncPlugin()]); + +const TESTID_INPUT = "in1"; +const TESTID_DISPLAY = "d1"; + +function Component1() { + const [count, setCount] = useMyRGS(); + const handleChange = useCallback( + (e: ChangeEvent) => { + setCount(parseInt(e.target.value)); + }, + [setCount], + ); + return ( +
+ +
+ ); +} + +function Component2() { + const [count] = useMyRGS(); + console.log("c2", { count }); + return

{count}

; +} + +describe("React18GlobalStore", () => { + test("check state update to multiple components", 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"); + expect(JSON.parse(localStorage.getItem(COUNTER_RGS_KEY) ?? "{}").val).toBe(5); + }); + + 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 afd0de1c..ecd7751e 100644 --- a/lib/r18gs/tests/use-rgs.test.tsx +++ b/lib/r18gs/tests/use-rgs.test.tsx @@ -3,8 +3,10 @@ import { act, fireEvent, render, screen } from "@testing-library/react"; import useRGS from "../src"; import { ChangeEvent, useCallback } from "react"; +const COUNT_RGS_KEY = "count"; + function Component1() { - const [count, setCount] = useRGS("count", 0); + const [count, setCount] = useRGS(COUNT_RGS_KEY, 0); const handleChange = useCallback( (e: ChangeEvent) => { setCount(parseInt(e.target.value)); @@ -19,7 +21,7 @@ function Component1() { } function Component2() { - const [count] = useRGS("count"); + const [count] = useRGS(COUNT_RGS_KEY); return

{count}

; } diff --git a/lib/r18gs/touchup.js b/lib/r18gs/touchup.js deleted file mode 100644 index 682a7528..00000000 --- a/lib/r18gs/touchup.js +++ /dev/null @@ -1,69 +0,0 @@ -/* eslint-disable -- no need - external file */ -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const packageJson = require(path.resolve(__dirname, "package.json")); -if (process.env.TOKEN) { - const { Octokit } = require("octokit"); - // Octokit.js - // https://github.com/octokit/core.js#readme - const octokit = new Octokit({ - auth: process.env.TOKEN, - }); - - const octoOptions = { - owner: process.env.OWNER, - repo: process.env.REPO, - headers: { - "X-GitHub-Api-Version": "2022-11-28", - }, - }; - const tag_name = `v${packageJson.version}`; - const name = `Release ${tag_name}`; - /** Create a release */ - octokit.request("POST /repos/{owner}/{repo}/releases", { - ...octoOptions, - tag_name, - target_commitish: "main", - name, - draft: false, - prerelease: false, - generate_release_notes: true, - headers: { - "X-GitHub-Api-Version": "2022-11-28", - }, - }); -} - -packageJson.main = "index.js"; -packageJson.types = "index.d.ts"; -packageJson.module = "index.mjs"; - -fs.writeFileSync( - path.resolve(__dirname, "dist", "package.json"), - JSON.stringify(packageJson, null, 2), -); - -fs.copyFileSync( - path.resolve(__dirname, "..", "..", "README.md"), - path.resolve(__dirname, "dist", "README.md"), -); - -fs.copyFileSync( - path.resolve(__dirname, "CHANGELOG.md"), - path.resolve(__dirname, "dist", "CHANGELOG.md"), -); - -const dirs = [path.resolve(__dirname, "dist")]; - -while (dirs.length) { - const dir = dirs.shift(); - fs.readdirSync(dir).forEach(f => { - const f1 = path.resolve(dir, f); - if (f.includes(".test.")) fs.unlink(f1, () => {}); - else if (fs.lstatSync(f1).isDirectory()) { - dirs.push(f1); - } - }); -} diff --git a/lib/r18gs/tsconfig-build.json b/lib/r18gs/tsconfig-build.json index 3785799b..fd87a4f2 100644 --- a/lib/r18gs/tsconfig-build.json +++ b/lib/r18gs/tsconfig-build.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "dist", "emitDeclarationOnly": true, - "sourceMap": false + "declarationMap": false }, "include": ["src"], "exclude": ["dist", "build", "node_modules", "**/*.test.*", "**/*.spec.*"] diff --git a/lib/r18gs/tsup.config.ts b/lib/r18gs/tsup.config.ts index ad68c6f4..7eca4d05 100644 --- a/lib/r18gs/tsup.config.ts +++ b/lib/r18gs/tsup.config.ts @@ -12,7 +12,7 @@ export default defineConfig(options => ({ { name: "improve-minify", setup(build) { - build.onLoad({ filter: /use-rgs.ts/ }, args => { + build.onLoad({ filter: /utils.ts/ }, args => { let contents = fs.readFileSync(args.path, "utf8"); const lines = contents.split("\n"); const hackLine = lines.find(line => line.startsWith("const [VALUE,")); @@ -30,7 +30,7 @@ export default defineConfig(options => ({ .split(",") .map((t, i) => ({ token: t.trim(), i })); - tokens.sort((a, b) => a.token.length - b.token.length); + tokens.sort((a, b) => b.token.length - a.token.length); for (const t of tokens) { contents = contents.replace(new RegExp(`${t.token}`, "g"), t.i + ""); diff --git a/packages/shared-ui/hooks/use-persistant-counter.ts b/packages/shared-ui/hooks/use-persistant-counter.ts new file mode 100644 index 00000000..5852f566 --- /dev/null +++ b/packages/shared-ui/hooks/use-persistant-counter.ts @@ -0,0 +1,6 @@ +import { create } from "r18gs/dist/with-plugins"; +import persistNSyncPlugin from "r18gs/dist/plugins/persist-and-sync"; + +export const useMyPersistantCounterStore = create("persistant-counter", 1, 1, [ + persistNSyncPlugin(), +]); diff --git a/packages/shared-ui/src/root/hero.tsx b/packages/shared-ui/src/root/hero.tsx index d1d37cee..bc8ee57d 100644 --- a/packages/shared-ui/src/root/hero.tsx +++ b/packages/shared-ui/src/root/hero.tsx @@ -2,6 +2,7 @@ import styles from "../root-layout.module.css"; import { Logo } from "../common/logo"; import Counter from "./counter"; import Display from "./display"; +import PersistantCounter from "./persistant-counter"; export function Hero() { return ( @@ -26,6 +27,10 @@ export function Hero() { +

Persistant counter - Also synced with multiple tabs

+
+ +
); diff --git a/packages/shared-ui/src/root/persistant-counter.tsx b/packages/shared-ui/src/root/persistant-counter.tsx new file mode 100644 index 00000000..48ea33c6 --- /dev/null +++ b/packages/shared-ui/src/root/persistant-counter.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { ChangeEvent, useCallback } from "react"; +import { useMyPersistantCounterStore } from "../../hooks/use-persistant-counter"; + +/** + * Persistant Counter + * + * @returns {JSX.Element} + */ +export default function PersistantCounter() { + const [count, setCount] = useMyPersistantCounterStore(); + + const handleChange = useCallback( + (e: ChangeEvent) => { + setCount(parseInt(e.target.value)); + }, + [setCount], + ); + return ( +
+

Persistant Counter

+

Duplicate this tab to see state sharing between tabs in action

+ +
+ ); +} diff --git a/turbo.json b/turbo.json index ac8b7ba9..e9a4620f 100644 --- a/turbo.json +++ b/turbo.json @@ -3,14 +3,18 @@ "globalDependencies": ["**/.env.*local"], "pipeline": { "build": { - "outputs": [".next/**", "!.next/cache/**"] + "outputs": [".next/**", "!.next/cache/**"], + "dependsOn": ["^build"] }, "lint": {}, - "test": {}, + "test": { + "dependsOn": ["^build"] + }, "doc": {}, "dev": { "cache": false, - "persistent": true + "persistent": true, + "dependsOn": ["^build"] } } }