├── .changeset ├── README.md └── config.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── publish.yml │ ├── sync.yml │ ├── test.yml │ └── upgrade.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── examples └── nextjs │ ├── .eslintrc.js │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── app │ ├── Counter.tsx │ ├── OpenNewTab.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── page.module.css │ ├── page.tsx │ └── store.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ └── favicon.ico │ └── tsconfig.json ├── package.json ├── packages ├── eslint-config-custom │ ├── README.md │ ├── library.js │ ├── next.js │ ├── package.json │ └── react-internal.js ├── persist-and-sync │ ├── CHANGELOG.md │ ├── __tests__ │ │ ├── index.test.ts │ │ └── store.ts │ ├── legacy-support.js │ ├── package.json │ ├── src │ │ └── index.ts │ ├── touchup.js │ ├── tsconfig.json │ ├── vitest.config.ts │ └── vitest.setup.ts └── tsconfig │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json ├── pnpm-workspace.yaml ├── tsconfig.json └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [mayank1513] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: [https://pages.razorpay.com/mayank1513] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | # publish only when package json has changed - assuming version upgrade 4 | on: 5 | push: 6 | branches: [main] 7 | paths: "packages/persist-and-sync/package.json" 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | packages: write 14 | contents: write 15 | id-token: write 16 | 17 | defaults: 18 | run: 19 | working-directory: ./packages/persist-and-sync 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | 26 | - uses: actions/setup-node@v4 27 | with: 28 | registry-url: https://registry.npmjs.org 29 | node-version: 20 30 | - run: npm i -g pnpm && pnpm i 31 | name: Install dependencies 32 | # fail and not publish if any of the unit tests are failing 33 | - name: Test 34 | run: pnpm test 35 | - name: Publish to NPM 36 | run: pnpm build && pnpm publish-package 37 | env: 38 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 39 | TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | OWNER: ${{ github.event.repository.owner.login }} 41 | REPO: ${{ github.event.repository.name }} 42 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | name: Sync 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | sync: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: write 10 | steps: 11 | - run: gh repo sync "mayank1513/persist-and-sync" -b "main" 12 | env: 13 | GH_TOKEN: ${{ github.token }} 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | - cron: "0 8 * * *" 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: 18 19 | - run: npm i -g pnpm && pnpm i 20 | name: Install dependencies 21 | - name: Run unit tests 22 | run: pnpm test 23 | - name: Upload coverage reports to Codecov 24 | uses: codecov/codecov-action@v4 25 | with: 26 | directory: ./packages/persist-and-sync 27 | token: ${{ secrets.CODECOV_TOKEN }} 28 | flags: fork-me 29 | - uses: paambaati/codeclimate-action@v5.0.0 30 | continue-on-error: true 31 | env: 32 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 33 | with: 34 | coverageLocations: ./packages/persist-and-sync/coverage/*.xml:clover 35 | -------------------------------------------------------------------------------- /.github/workflows/upgrade.yml: -------------------------------------------------------------------------------- 1 | name: Update dependencies 2 | 3 | on: 4 | schedule: 5 | - cron: "0 */8 * * *" 6 | jobs: 7 | update-deps: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: write 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | 16 | - uses: actions/setup-node@v4 17 | with: 18 | registry-url: https://registry.npmjs.org 19 | node-version: 20 20 | - name: Setup Git 21 | run: | 22 | git config --global user.name "mayank1513" 23 | git config --global user.email "mayank.srmu@gmail.com" 24 | git fetch 25 | git checkout main 26 | git pull 27 | - run: npm i -g pnpm && pnpm i --no-frozen-lockfile 28 | name: Install dependencies 29 | - run: git stash --include-untracked 30 | name: clean up working directory 31 | - run: pnpx @turbo/codemod update . && pnpm update --latest -r 32 | name: Update dependencies 33 | - run: pnpm build --filter @example/nextjs 34 | name: Build all apps to make sure it is not broken due to dependency upgrades 35 | - name: Save upgraded packages back to repo 36 | run: echo $(date +%F_%H:%M:%S) > .lst && git add . && git commit -m "upgrade deps && docs [skip ci]" && git push origin main 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # build files 21 | dist 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | 35 | # turbo 36 | .turbo 37 | 38 | # vercel 39 | .vercel 40 | 41 | # lock files 42 | *lock* 43 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *lock.* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "printWidth": 100, 4 | "tabWidth": 2, 5 | "arrowParens": "avoid", 6 | "jsxBracketSameLine": true, 7 | "bracketSameLine": true, 8 | "useTabs": true 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "mayank1513.trello-kanban-task-board"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Formatting using Prettier by default for all languages 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | // Formatting using Prettier for JavaScript, overrides VSCode default. 5 | "[javascript]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | 9 | // Ensure enough terminal history is preserved when running tests. 10 | "terminal.integrated.scrollback": 10000, 11 | 12 | // Configure todo-tree to exclude node_modules, dist, and compiled. 13 | "todo-tree.filtering.excludeGlobs": ["**/node_modules", "**/dist", "**/compiled"], 14 | // Match TODO-APP in addition to other TODOs. 15 | "todo-tree.general.tags": ["BUG", "HACK", "FIXME", "TODO", "XXX", "[ ]", "[x]", "TODO-APP"], 16 | 17 | // Disable TypeScript surveys. 18 | "typescript.surveys.enabled": false, 19 | 20 | "grammarly.selectors": [ 21 | { 22 | "language": "markdown", 23 | "scheme": "file" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mayank 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Persist-And-Sync Zustand Store 2 | 3 | [![test](https://github.com/react18-tools/persist-and-sync/actions/workflows/test.yml/badge.svg)](https://github.com/react18-tools/persist-and-sync/actions/workflows/test.yml) [![Maintainability](https://api.codeclimate.com/v1/badges/5355eb02cfedc9184e3f/maintainability)](https://codeclimate.com/github/mayank1513/persist-and-sync/maintainability) [![codecov](https://codecov.io/gh/mayank1513/persist-and-sync/graph/badge.svg)](https://codecov.io/gh/mayank1513/persist-and-sync) [![Version](https://img.shields.io/npm/v/persist-and-sync.svg?colorB=green)](https://www.npmjs.com/package/persist-and-sync) [![Downloads](https://img.jsdelivr.com/img.shields.io/npm/d18m/persist-and-sync.svg)](https://www.npmjs.com/package/persist-and-sync) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/persist-and-sync) 4 | 5 | > Zustand middleware to easily persist and sync Zustand state between tabs/windows/iframes (Same Origin) 6 | 7 | > Motivation: Recently I got caught up in several issues working with the persist middleware and syncing tabs with Zustand. This is a simple lightweight middleware to persist and instantly share state between tabs or windows 8 | 9 | - ✅ 🐙 ~ 1 kB size cross-tab state sharing + persistence for zustand 10 | - ✅ Full TypeScript Support 11 | - ✅ solid reliability in 1 writing and n reading tab scenarios (with changing writing tab) 12 | - ✅ Fire and forget approach of always using the latest state. Perfect for single-user systems 13 | - ✅ Share state between multiple browsing contexts 14 | - ✅ Additional control over which fields to `persist-and-sync` and which to ignore 15 | - ✅ Optimized for performance using memoization and closures. 16 | - ✅ Update options at runtime by setting `__persistNSyncOptions` in your store. 17 | 18 | ## Install 19 | 20 | ```bash 21 | $ pnpm add persist-and-sync 22 | ``` 23 | 24 | **or** 25 | 26 | ```bash 27 | $ npm install persist-and-sync 28 | ``` 29 | 30 | **or** 31 | 32 | ```bash 33 | $ yarn add persist-and-sync 34 | ``` 35 | 36 | ## Usage 37 | 38 | Add the middleware while creating the store and the rest will be taken care. 39 | 40 | ```ts 41 | import { create } from "zustand"; 42 | import { persistNSync } from "persist-and-sync"; 43 | 44 | type MyStore = { 45 | count: number; 46 | set: (n: number) => void; 47 | }; 48 | 49 | const useStore = create( 50 | persistNSync( 51 | set => ({ 52 | count: 0, 53 | set: n => set({ count: n }), 54 | }), 55 | { name: "my-example" }, 56 | ), 57 | ); 58 | ``` 59 | 60 | ⚡🎉Boom! Just a couple of lines and your state perfectly syncs between tabs/windows and it is also persisted using `localStorage`! 61 | 62 | ## Advanced Usage (Customizations) 63 | 64 | ### PersistNSyncOptions 65 | 66 | In several cases, you might want to exclude several fields from syncing. To support this scenario, we provide a mechanism to exclude fields based on a list of fields or regular expressions. 67 | 68 | ```typescript 69 | type PersistNSyncOptionsType = { 70 | name: string; 71 | /** @deprecated */ 72 | regExpToIgnore?: RegExp; 73 | include?: (string | RegExp)[]; 74 | exclude?: (string | RegExp)[]; 75 | storage?: "localStorage" | "sessionStorage" | "cookies" /** Added in v1.1.0 */; 76 | }; 77 | ``` 78 | 79 | **Example** 80 | 81 | ```typescript 82 | export const useMyStore = create()( 83 | persistNSync( 84 | set => ({ 85 | count: 0, 86 | _count: 0 /** skipped as it is included in exclude array */, 87 | setCount: count => { 88 | set(state => ({ ...state, count })); 89 | }, 90 | set_Count: _count => { 91 | set(state => ({ ...state, _count })); 92 | }, 93 | }), 94 | { name: "example", exclude: ["_count"] }, 95 | ), 96 | ); 97 | ``` 98 | 99 | > It is good to note here that each element of `include` and `exclude` array can either be a string or a regular expression. 100 | > To use regular expression, you should either use `new RegExp()` or `/your-expression/` syntax. Double or single quoted strings are not treated as regular expression. 101 | > You can specify whether to use either `"localStorage"`, `"sessionStorage"`, or `"cookies"` to persist the state - default `"localStorage"`. Please note that `"sessionStorage"` is not persisted. Hence can be used for sync only scenarios. 102 | 103 | ### Updating options at runtime 104 | 105 | Since version 1.2, you can also update the options at runTime by setting `__persistNSyncOptions` in your Zustand state. 106 | 107 | **Example** 108 | 109 | ```ts 110 | interface StoreWithOptions { 111 | count: number; 112 | _count: number; 113 | __persistNSyncOptions: PersistNSyncOptionsType; 114 | setCount: (c: number) => void; 115 | set_Count: (c: number) => void; 116 | setOptions: (__persistNSyncOptions: PersistNSyncOptionsType) => void; 117 | } 118 | 119 | const defaultOptions = { name: "example", include: [/count/], exclude: [/^_/] }; 120 | 121 | export const useStoreWithOptions = create( 122 | persistNSync( 123 | set => ({ 124 | count: 0, 125 | _count: 0 /** skipped as it matches the regexp provided */, 126 | __persistNSyncOptions: defaultOptions, 127 | setCount: count => set(state => ({ ...state, count })), 128 | set_Count: _count => set(state => ({ ...state, _count })), 129 | setOptions: __persistNSyncOptions => set(state => ({ ...state, __persistNSyncOptions })), 130 | }), 131 | defaultOptions, 132 | ), 133 | ); 134 | ``` 135 | 136 | ### Clear Storage 137 | 138 | Starting from version 1.2, you can also clear the persisted data by calling `clearStorage` function. It takes `name` of your store (`name` passed in `options` while creating the store), and optional `storageType` parameters. 139 | 140 | ```ts 141 | import { clearStorage } from "persist-and-sync"; 142 | 143 | ... 144 | clearStorage("my-store", "cookies"); 145 | ... 146 | ``` 147 | 148 | ## Legacy / Deprecated 149 | 150 | #### Ignore/filter out fields based on regExp 151 | 152 | In several cases, you might want to exclude several fields from syncing. To support this scenario, we provide a mechanism to exclude fields based on regExp. Just pass `regExpToIgnore` (optional - default -> undefined) in the options object. 153 | 154 | ```ts 155 | // to ignore fields containing a slug 156 | persistNSync( 157 | set => ({ 158 | count: 0, 159 | slugSomeState: 1, 160 | slugSomeState2: 1, 161 | set: n => set({ count: n }), 162 | }), 163 | { name: "my-channel", regExpToIgnore: /slug/ }, 164 | // or regExpToIgnore: new RegExp('slug') 165 | // Use full power of regExp by adding `i` and `g` flags 166 | ), 167 | ``` 168 | 169 | For more details about regExp check out - [JS RegExp](https://www.w3schools.com/jsref/jsref_obj_regexp.asp) 170 | 171 | ### Exact match 172 | 173 | For exactly matching a parameter/field use `/^your-field-name$/`. `^` forces match from the first character and similarly, `$` forces match until the last character. 174 | 175 | ### Ignore multiple fields with exact match 176 | 177 | use `regExpToIgnore: /^(field1|field2|field3)$/` 178 | 179 | ### 🤩 Don't forget to star [this repo](https://github.com/mayank1513/persist-and-sync)! 180 | 181 | Want a 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) 182 | 183 | ## License 184 | 185 | Licensed as MIT open source. 186 | 187 |
188 | 189 |

with 💖 by Mayank Kumar Chaudhari

190 | -------------------------------------------------------------------------------- /examples/nextjs/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["custom/next"], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /examples/nextjs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # nextjs-example 2 | 3 | ## 1.0.8 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies 8 | - persist-and-sync@1.2.3 9 | 10 | ## 1.0.7 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies 15 | - persist-and-sync@1.2.2 16 | 17 | ## 1.0.6 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies 22 | - persist-and-sync@1.2.1 23 | 24 | ## 1.0.5 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [933c240] 29 | - persist-and-sync@1.2.0 30 | 31 | ## 1.0.4 32 | 33 | ### Patch Changes 34 | 35 | - Updated dependencies 36 | - persist-and-sync@1.1.2 37 | 38 | ## 1.0.3 39 | 40 | ### Patch Changes 41 | 42 | - Updated dependencies 43 | - persist-and-sync@1.1.1 44 | 45 | ## 1.0.2 46 | 47 | ### Patch Changes 48 | 49 | - Updated dependencies 50 | - persist-and-sync@1.1.0 51 | 52 | ## 1.0.1 53 | 54 | ### Patch Changes 55 | 56 | - Added inlcude and exclude arrays to control fields to include or exclude from persist and syncing. Deprecated `regExpToIgnore` option. 57 | - Updated dependencies 58 | - persist-and-sync@1.0.0 59 | -------------------------------------------------------------------------------- /examples/nextjs/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | First, run the development server: 4 | 5 | ```bash 6 | yarn dev 7 | ``` 8 | 9 | Open [http://localhost:3001](http://localhost:3001) with your browser to see the result. 10 | 11 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 12 | 13 | To create [API routes](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) add an `api/` directory to the `app/` directory with a `route.ts` file. For individual endpoints, create a subfolder in the `api` directory, like `api/hello/route.ts` would map to [http://localhost:3001/api/hello](http://localhost:3001/api/hello). 14 | 15 | ## Learn More 16 | 17 | To learn more about Next.js, take a look at the following resources: 18 | 19 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 20 | - [Learn Next.js](https://www.udemy.com/course/react-and-next-js-with-typescript/?referralCode=7202184A1E57C3DCA8B2) - an interactive Next.js course. 21 | 22 | You can check out [the Turbo Template GitHub repository](https://github.com/mayank1513/turbo-template/) - your feedback and contributions are welcome! 23 | 24 | ### 🤩 Don't forger to start this repo! 25 | 26 |
27 | 28 |

with 💖 by Mayank Kumar Chaudhari

29 | -------------------------------------------------------------------------------- /examples/nextjs/app/Counter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useMyStore } from "./store"; 3 | import styles from "./page.module.css"; 4 | 5 | interface CounterProps { 6 | synced?: boolean; 7 | } 8 | 9 | export default function Counter({ synced = false }: CounterProps) { 10 | const [count, setCount] = useMyStore(state => 11 | synced ? [state.count, state.setCount] : [state._count, state.set_Count], 12 | ); 13 | return ( 14 |
15 |

{synced ? "" : "Not "}Synced Counter:

16 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /examples/nextjs/app/OpenNewTab.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | export default function OpenNewTab() { 3 | return ( 4 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /examples/nextjs/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react18-tools/persist-and-sync/468ef9b8af116ad34a9eb2fe1fb460256730f426/examples/nextjs/app/favicon.ico -------------------------------------------------------------------------------- /examples/nextjs/app/globals.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | background: #1d232a; 5 | color: #ca8a04; 6 | } 7 | * { 8 | box-sizing: border-box; 9 | } 10 | -------------------------------------------------------------------------------- /examples/nextjs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | 3 | export default function RootLayout({ children }: { children: React.ReactNode }) { 4 | return ( 5 | 6 | {children} 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /examples/nextjs/app/page.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100vw; 3 | height: 100vh; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: space-evenly; 8 | } 9 | .card { 10 | background: #2a323c; 11 | border-radius: 10px; 12 | display: flex; 13 | gap: 20px; 14 | align-items: center; 15 | width: 400px; 16 | justify-content: space-between; 17 | padding: 0 40px; 18 | } 19 | -------------------------------------------------------------------------------- /examples/nextjs/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Counter from "./Counter"; 2 | import OpenNewTab from "./OpenNewTab"; 3 | import styles from "./page.module.css"; 4 | 5 | export default function Page() { 6 | return ( 7 |
8 |
9 |

Zustand Persist and Sync Next.js Example

10 |
11 | 12 | 13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/nextjs/app/store.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persistNSync } from "persist-and-sync"; 3 | 4 | interface MyStoreType { 5 | count: number; 6 | _count: number; 7 | setCount: (c: number) => void; 8 | set_Count: (c: number) => void; 9 | } 10 | 11 | export const useMyStore = create()( 12 | persistNSync( 13 | set => ({ 14 | count: 0, 15 | _count: 0 /** skipped as it matches the regexp provided */, 16 | setCount: count => { 17 | set(state => ({ ...state, count })); 18 | }, 19 | set_Count: _count => { 20 | set(state => ({ ...state, _count })); 21 | }, 22 | }), 23 | { name: "example", exclude: ["_count"], storage: "cookies" }, 24 | ), 25 | ); 26 | -------------------------------------------------------------------------------- /examples/nextjs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | transpilePackages: ["ui"], 4 | }; 5 | -------------------------------------------------------------------------------- /examples/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-example", 3 | "version": "1.0.8", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --port 3001", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "next": "^15.3.2", 13 | "persist-and-sync": "workspace:*", 14 | "react": "^19.1.0", 15 | "react-dom": "^19.1.0", 16 | "zustand": "^5.0.5" 17 | }, 18 | "devDependencies": { 19 | "@next/eslint-plugin-next": "^15.3.2", 20 | "@types/node": "^22.15.21", 21 | "@types/react": "^19.1.5", 22 | "@types/react-dom": "^19.1.5", 23 | "eslint-config-custom": "workspace:*", 24 | "tsconfig": "workspace:*", 25 | "typescript": "^5.8.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/nextjs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react18-tools/persist-and-sync/468ef9b8af116ad34a9eb2fe1fb460256730f426/examples/nextjs/public/favicon.ico -------------------------------------------------------------------------------- /examples/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "compilerOptions": { 4 | "plugins": [{ "name": "next" }] 5 | }, 6 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "turbo run build", 5 | "dev": "turbo run dev", 6 | "lint": "turbo run lint", 7 | "test": "turbo run test", 8 | "format": "prettier --write \"**/*.{ts,tsx,js,jsx,md,css,scss}\"" 9 | }, 10 | "devDependencies": { 11 | "@changesets/cli": "^2.29.4", 12 | "eslint": "^9.27.0", 13 | "prettier": "^3.5.3", 14 | "tsconfig": "workspace:*", 15 | "turbo": "^2.5.3" 16 | }, 17 | "name": "turbo-template", 18 | "packageManager": "pnpm@10.11.0" 19 | } 20 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/README.md: -------------------------------------------------------------------------------- 1 | # `@turbo/eslint-config` 2 | 3 | Collection of internal eslint configurations. 4 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/library.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use with 7 | * typescript packages. 8 | * 9 | * This config extends the Vercel Engineering Style Guide. 10 | * For more information, see https://github.com/vercel/style-guide 11 | * 12 | */ 13 | 14 | module.exports = { 15 | extends: ["@vercel/style-guide/eslint/node", "@vercel/style-guide/eslint/typescript"].map( 16 | require.resolve, 17 | ), 18 | parserOptions: { 19 | project, 20 | }, 21 | globals: { 22 | React: true, 23 | JSX: true, 24 | }, 25 | settings: { 26 | "import/resolver": { 27 | typescript: { 28 | project, 29 | }, 30 | }, 31 | }, 32 | ignorePatterns: ["node_modules/", "dist/"], 33 | }; 34 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/next.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use with 7 | * Next.js apps. 8 | * 9 | * This config extends the Vercel Engineering Style Guide. 10 | * For more information, see https://github.com/vercel/style-guide 11 | * 12 | */ 13 | 14 | module.exports = { 15 | extends: [ 16 | "@vercel/style-guide/eslint/node", 17 | "@vercel/style-guide/eslint/typescript", 18 | "@vercel/style-guide/eslint/browser", 19 | "@vercel/style-guide/eslint/react", 20 | "@vercel/style-guide/eslint/next", 21 | "eslint-config-turbo", 22 | ].map(require.resolve), 23 | parserOptions: { 24 | project, 25 | }, 26 | globals: { 27 | React: true, 28 | JSX: true, 29 | }, 30 | settings: { 31 | "import/resolver": { 32 | typescript: { 33 | project, 34 | }, 35 | }, 36 | }, 37 | ignorePatterns: ["node_modules/", "dist/"], 38 | // add rules configurations here 39 | rules: { 40 | "import/no-default-export": "off", 41 | "unicorn/filename-case": "off", 42 | camelcase: "off", 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-custom", 3 | "license": "MIT", 4 | "version": "0.0.0", 5 | "private": true, 6 | "devDependencies": { 7 | "@vercel/style-guide": "^6.0.0", 8 | "eslint-config-turbo": "^2.5.3" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/react-internal.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use with 7 | * internal (bundled by their consumer) libraries 8 | * that utilize React. 9 | * 10 | * This config extends the Vercel Engineering Style Guide. 11 | * For more information, see https://github.com/vercel/style-guide 12 | * 13 | */ 14 | 15 | module.exports = { 16 | extends: [ 17 | "@vercel/style-guide/eslint/browser", 18 | "@vercel/style-guide/eslint/typescript", 19 | "@vercel/style-guide/eslint/react", 20 | ].map(require.resolve), 21 | parserOptions: { 22 | project, 23 | }, 24 | globals: { 25 | JSX: true, 26 | }, 27 | settings: { 28 | "import/resolver": { 29 | typescript: { 30 | project, 31 | }, 32 | }, 33 | }, 34 | ignorePatterns: ["node_modules/", "dist/", ".eslintrc.js"], 35 | // add rules configurations here 36 | rules: { 37 | "import/no-default-export": "off", 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/persist-and-sync/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # persist-and-sync 2 | 3 | ## 1.2.3 4 | 5 | ### Patch Changes 6 | 7 | - Upgrade Zustand support to v5 8 | 9 | ## 1.2.2 10 | 11 | ### Patch Changes 12 | 13 | - Update peerDependencies 14 | 15 | ## 1.2.1 16 | 17 | ### Patch Changes 18 | 19 | - Add provance 20 | 21 | ## 1.2.0 22 | 23 | ### Minor Changes 24 | 25 | - 933c240: Add ability to change options at Runtime & add clearItem method 26 | 27 | ## 1.1.2 28 | 29 | ### Patch Changes 30 | 31 | - Update dependencies 32 | 33 | ## 1.1.1 34 | 35 | ### Patch Changes 36 | 37 | - Updated internal sync mechanism 38 | 39 | ## 1.1.0 40 | 41 | ### Minor Changes 42 | 43 | - Added option to select storage - localStorage, sessionStorage or cookies. 44 | 45 | ## 1.0.0 46 | 47 | ### Major Changes 48 | 49 | - Added inlcude and exclude arrays to control fields to include or exclude from persist and syncing. Deprecated `regExpToIgnore` option. 50 | -------------------------------------------------------------------------------- /packages/persist-and-sync/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { act, cleanup, fireEvent, renderHook } from "@testing-library/react"; 2 | 3 | import { afterEach, describe, test } from "vitest"; 4 | import { useCookieStore, useMyStore, useStoreWithOptions } from "./store"; 5 | 6 | describe.concurrent("Persist and Sync", () => { 7 | afterEach(cleanup); 8 | test("test initial state", async ({ expect }) => { 9 | const { result } = renderHook(() => useMyStore()); 10 | expect(result.current.count).toBe(0); 11 | }); 12 | 13 | test("test setting state", async ({ expect }) => { 14 | const { result } = renderHook(() => useCookieStore()); 15 | act(() => result.current.setCount(5)); 16 | expect(result.current.count).toBe(5); 17 | expect(localStorage.getItem("example")).toBe('{"count":5}'); 18 | }); 19 | 20 | test("test exclude key", async ({ expect }) => { 21 | const { result } = renderHook(() => useCookieStore()); 22 | act(() => result.current.set_Count(6)); 23 | expect(result.current._count).toBe(6); 24 | expect(localStorage.getItem("example")).not.toBe('{"count":6}'); 25 | }); 26 | 27 | test("store with __persistNSyncOptions", async ({ expect }) => { 28 | const { result } = renderHook(() => useStoreWithOptions()); 29 | act(() => result.current.set_Count(6)); 30 | expect(result.current._count).toBe(6); 31 | expect(localStorage.getItem("example")).not.toContain('"_count":6'); 32 | act(() => 33 | result.current.setOptions({ 34 | ...result.current.__persistNSyncOptions, 35 | include: [], 36 | exclude: [], 37 | }), 38 | ); 39 | 40 | act(() => result.current.set_Count(10)); 41 | expect(result.current._count).toBe(10); 42 | expect(localStorage.getItem("example")).toContain('"_count":10'); 43 | }); 44 | 45 | test("Change storage type", async ({ expect }) => { 46 | const { result } = renderHook(() => useStoreWithOptions()); 47 | act(() => result.current.setCount(6)); 48 | expect(result.current.count).toBe(6); 49 | expect(localStorage.getItem("example")).toContain('"count":6'); 50 | act(() => 51 | result.current.setOptions({ 52 | ...result.current.__persistNSyncOptions, 53 | storage: "sessionStorage", 54 | }), 55 | ); 56 | 57 | act(() => 58 | result.current.setOptions({ 59 | ...result.current.__persistNSyncOptions, 60 | storage: "cookies", 61 | }), 62 | ); 63 | 64 | act(() => result.current.setCount(120)); 65 | 66 | act(() => 67 | result.current.setOptions({ 68 | ...result.current.__persistNSyncOptions, 69 | storage: "localStorage", 70 | }), 71 | ); 72 | 73 | act(() => result.current.setCount(20)); 74 | expect(result.current.count).toBe(20); 75 | expect(localStorage.getItem("example")).toContain('"count":20'); 76 | }); 77 | 78 | test("Storage event", async ({ expect }) => { 79 | const hook = renderHook(() => useMyStore()); 80 | await act(() => 81 | fireEvent(window, new StorageEvent("storage", { key: "example", newValue: '{"count":6}' })), 82 | ); 83 | expect(hook.result.current.count).toBe(6); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /packages/persist-and-sync/__tests__/store.ts: -------------------------------------------------------------------------------- 1 | import { create } from "../vitest.setup"; 2 | import { PersistNSyncOptionsType, persistNSync } from "../src"; 3 | 4 | interface MyStoreType { 5 | count: number; 6 | _count: number; 7 | setCount: (c: number) => void; 8 | set_Count: (c: number) => void; 9 | } 10 | 11 | export const useMyStore = create( 12 | persistNSync( 13 | set => ({ 14 | count: 0, 15 | _count: 0 /** skipped as it matches the regexp provided */, 16 | setCount: count => set(state => ({ ...state, count })), 17 | set_Count: _count => set(state => ({ ...state, _count })), 18 | }), 19 | { name: "example", exclude: [/^_/], initDelay: 0 }, 20 | ), 21 | ); 22 | 23 | export const useCookieStore = create( 24 | persistNSync( 25 | set => ({ 26 | count: 0, 27 | _count: 0 /** skipped as it matches the regexp provided */, 28 | setCount: count => set(state => ({ ...state, count })), 29 | set_Count: _count => set(state => ({ ...state, _count })), 30 | }), 31 | { name: "example", include: [/count/], exclude: [/^_/], storage: "cookies" }, 32 | ), 33 | ); 34 | 35 | interface StoreWithOptions { 36 | count: number; 37 | _count: number; 38 | __persistNSyncOptions: PersistNSyncOptionsType; 39 | setCount: (c: number) => void; 40 | set_Count: (c: number) => void; 41 | setOptions: (__persistNSyncOptions: PersistNSyncOptionsType) => void; 42 | } 43 | 44 | const defaultOptions = { name: "example", include: [/count/], exclude: [/^_/] }; 45 | 46 | export const useStoreWithOptions = create( 47 | persistNSync( 48 | set => ({ 49 | count: 0, 50 | _count: 0 /** skipped as it matches the regexp provided */, 51 | __persistNSyncOptions: defaultOptions, 52 | setCount: count => set(state => ({ ...state, count })), 53 | set_Count: _count => set(state => ({ ...state, _count })), 54 | setOptions: __persistNSyncOptions => set(state => ({ ...state, __persistNSyncOptions })), 55 | }), 56 | defaultOptions, 57 | ), 58 | ); 59 | -------------------------------------------------------------------------------- /packages/persist-and-sync/legacy-support.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const packageJson = require(path.resolve(__dirname, "package.json")); 6 | 7 | delete packageJson.scripts; 8 | packageJson.main = packageJson.main.split("/")[1]; 9 | packageJson.types = packageJson.types.split("/")[1]; 10 | packageJson.name = "persistnsync"; 11 | 12 | fs.writeFileSync( 13 | path.resolve(__dirname, "dist", "package.json"), 14 | JSON.stringify(packageJson, null, 2), 15 | ); 16 | 17 | -------------------------------------------------------------------------------- /packages/persist-and-sync/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "persist-and-sync", 3 | "author": "Mayank Kumar Chaudhari ", 4 | "version": "1.2.3", 5 | "description": "Zustand middleware to easily persist and sync Zustand state between tabs and windows", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/react18-tools/persist-and-sync.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/react18-tools/persist-and-sync/issues" 14 | }, 15 | "homepage": "https://github.com/react18-tools/persist-and-sync#readme", 16 | "sideEffects": false, 17 | "license": "MIT", 18 | "scripts": { 19 | "build": "tsc && node touchup.js", 20 | "publish-package": "cd dist && npm publish && node ../legacy-support.js && npm publish --provenance --access public", 21 | "test": "vitest run --coverage" 22 | }, 23 | "funding": { 24 | "type": "github", 25 | "url": "https://github.com/sponsors/mayank1513" 26 | }, 27 | "devDependencies": { 28 | "@testing-library/react": "^16.3.0", 29 | "@types/node": "^22.15.21", 30 | "@vitejs/plugin-react": "^4.4.1", 31 | "@vitest/coverage-v8": "^3.1.4", 32 | "jsdom": "^26.1.0", 33 | "octokit": "^5.0.2", 34 | "typescript": "^5.8.3", 35 | "vitest": "^3.1.4", 36 | "zustand": "^5.0.5" 37 | }, 38 | "peerDependencies": { 39 | "zustand": ">=3" 40 | }, 41 | "keywords": [ 42 | "web", 43 | "api", 44 | "broadcast", 45 | "channel", 46 | "sync-tabs", 47 | "sync-windows", 48 | "sync", 49 | "broadcast-channel", 50 | "persist", 51 | "localStorage", 52 | "hooks", 53 | "react", 54 | "react 18", 55 | "zustand", 56 | "middleware", 57 | "state", 58 | "optimized", 59 | "tiny", 60 | "typescript", 61 | "javascript", 62 | "mayank1513" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /packages/persist-and-sync/src/index.ts: -------------------------------------------------------------------------------- 1 | import { StateCreator } from "zustand"; 2 | 3 | export type StorageType = "localStorage" | "sessionStorage" | "cookies"; 4 | 5 | export interface PersistNSyncOptionsType { 6 | name: string; 7 | /** @deprecated */ 8 | regExpToIgnore?: RegExp; 9 | include?: (string | RegExp)[]; 10 | exclude?: (string | RegExp)[]; 11 | storage?: StorageType; 12 | /** @defaultValue 100 */ 13 | initDelay?: number; 14 | } 15 | 16 | type PersistNSyncType = ( 17 | f: StateCreator, 18 | options: PersistNSyncOptionsType, 19 | ) => StateCreator; 20 | 21 | const DEFAULT_INIT_DELAY = 100; 22 | 23 | function getItem(options: PersistNSyncOptionsType) { 24 | const cookies = document.cookie.split("; "); 25 | const cookie = cookies.find(c => c.startsWith(options.name)); 26 | return ( 27 | localStorage.getItem(options.name) || 28 | sessionStorage.getItem(options.name) || 29 | cookie?.split("=")[1] 30 | ); 31 | } 32 | 33 | function setItem(options: PersistNSyncOptionsType, value: string) { 34 | const { storage } = options; 35 | if (storage === "cookies") { 36 | document.cookie = `${options.name}=${value}; max-age=31536000; SameSite=Strict;`; 37 | } 38 | if (storage === "sessionStorage") sessionStorage.setItem(options.name, value); 39 | else localStorage.setItem(options.name, value); 40 | } 41 | 42 | export function clearStorage(name: string, storage?: StorageType) { 43 | switch (storage || "localStorage") { 44 | case "localStorage": 45 | localStorage.removeItem(name); 46 | break; 47 | case "sessionStorage": 48 | sessionStorage.removeItem(name); 49 | break; 50 | case "cookies": 51 | document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:01 GMT; SameSite=Strict;`; 52 | break; 53 | default: 54 | } 55 | } 56 | 57 | export const persistNSync: PersistNSyncType = (stateCreator, options) => (set, get, store) => { 58 | /** avoid error during serverside render */ 59 | if (!globalThis.localStorage) return stateCreator(set, get, store); 60 | 61 | if (!options.storage) options.storage = "localStorage"; 62 | 63 | /** timeout 0 is enough. timeout 100 is added to avoid server and client render content mismatch error */ 64 | const delay = options.initDelay === undefined ? DEFAULT_INIT_DELAY : options.initDelay; 65 | setTimeout(() => { 66 | const initialState = get() as Record; 67 | const savedState = getItem(options); 68 | if (savedState) set({ ...initialState, ...JSON.parse(savedState) }); 69 | }, delay); 70 | 71 | const set_: typeof set = (newStateOrPartialOrFunction, replace) => { 72 | const prevState = get() as Record; 73 | // @ts-expect-error -- Zustand v5 introduced stricter type checking 74 | set(newStateOrPartialOrFunction, replace); 75 | const newState = get() as Record; 76 | saveAndSync({ newState, prevState, options }); 77 | }; 78 | 79 | window.addEventListener("storage", e => { 80 | if (e.key === options.name) set({ ...get(), ...JSON.parse(e.newValue || "{}") }); 81 | }); 82 | return stateCreator(set_, get, store); 83 | }; 84 | 85 | interface SaveAndSyncProps { 86 | newState: Record; 87 | prevState: Record; 88 | options: PersistNSyncOptionsType; 89 | } 90 | 91 | /** Encapsulate cache in closure */ 92 | const getKeysToPersistAndSyncMemoised = (() => { 93 | const persistAndSyncKeysCache: { [k: string]: string[] } = {}; 94 | 95 | const getKeysToPersistAndSync = (keys: string[], options: PersistNSyncOptionsType) => { 96 | const { exclude, include } = options; 97 | 98 | const keysToInlcude = include?.length 99 | ? keys.filter(key => matchPatternOrKey(key, include)) 100 | : keys; 101 | 102 | const keysToPersistAndSync = keysToInlcude.filter( 103 | key => !matchPatternOrKey(key, exclude || []), 104 | ); 105 | return keysToPersistAndSync; 106 | }; 107 | 108 | return (keys: string[], options: PersistNSyncOptionsType) => { 109 | const cacheKey = JSON.stringify({ options, keys }); 110 | if (!persistAndSyncKeysCache[cacheKey]) 111 | persistAndSyncKeysCache[cacheKey] = getKeysToPersistAndSync(keys, options); 112 | return persistAndSyncKeysCache[cacheKey]; 113 | }; 114 | })(); 115 | 116 | function matchPatternOrKey(key: string, patterns: (string | RegExp)[]) { 117 | for (const patternOrKey of patterns) { 118 | if (typeof patternOrKey === "string" && key === patternOrKey) return true; 119 | else if (patternOrKey instanceof RegExp && patternOrKey.test(key)) return true; 120 | } 121 | return false; 122 | } 123 | 124 | function saveAndSync({ newState, prevState, options }: SaveAndSyncProps) { 125 | if (newState.__persistNSyncOptions) { 126 | const prevStorage = prevState.__persistNSyncOptions?.storage || options.storage; 127 | const newStorage = newState.__persistNSyncOptions?.storage || options.storage; 128 | if (prevStorage !== newStorage) { 129 | const name = prevState.__persistNSyncOptions.name || options.name; 130 | clearStorage(name, prevStorage); 131 | } 132 | Object.assign(options, newState.__persistNSyncOptions); 133 | } 134 | 135 | /** temporarily support `regExpToIgnore` */ 136 | if (!options.exclude) options.exclude = []; 137 | if (options.regExpToIgnore) options.exclude.push(options.regExpToIgnore); 138 | /** end of temporarily support `regExpToIgnore` */ 139 | 140 | const keysToPersistAndSync = getKeysToPersistAndSyncMemoised(Object.keys(newState), options); 141 | 142 | if (keysToPersistAndSync.length === 0) return; 143 | 144 | const stateToStore: Record = {}; 145 | keysToPersistAndSync 146 | .filter(key => prevState[key] !== newState[key]) // using only shallow equality 147 | .forEach(key => (stateToStore[key] = newState[key])); 148 | if (Object.keys(stateToStore).length) setItem(options, JSON.stringify(stateToStore)); 149 | } 150 | -------------------------------------------------------------------------------- /packages/persist-and-sync/touchup.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const packageJson = require(path.resolve(__dirname, "package.json")); 6 | if (process.env.TOKEN) { 7 | const { Octokit } = require("octokit"); 8 | // Octokit.js 9 | // https://github.com/octokit/core.js#readme 10 | const octokit = new Octokit({ 11 | auth: process.env.TOKEN, 12 | }); 13 | 14 | const octoOptions = { 15 | owner: process.env.OWNER, 16 | repo: process.env.REPO, 17 | headers: { 18 | "X-GitHub-Api-Version": "2022-11-28", 19 | }, 20 | }; 21 | const tagName = `v${packageJson.version}`; 22 | const name = `Release ${tagName}`; 23 | /** Create a release */ 24 | octokit.request("POST /repos/{owner}/{repo}/releases", { 25 | ...octoOptions, 26 | tag_name: tagName, 27 | target_commitish: "main", 28 | name, 29 | draft: false, 30 | prerelease: false, 31 | generate_release_notes: true, 32 | headers: { 33 | "X-GitHub-Api-Version": "2022-11-28", 34 | }, 35 | }); 36 | } 37 | delete packageJson.scripts; 38 | packageJson.main = packageJson.main.split("/")[1]; 39 | packageJson.types = packageJson.types.split("/")[1]; 40 | 41 | fs.writeFileSync( 42 | path.resolve(__dirname, "dist", "package.json"), 43 | JSON.stringify(packageJson, null, 2), 44 | ); 45 | 46 | fs.copyFileSync( 47 | path.resolve(__dirname, "..", "..", "README.md"), 48 | path.resolve(__dirname, "dist", "README.md"), 49 | ); 50 | -------------------------------------------------------------------------------- /packages/persist-and-sync/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "moduleResolution": "node", 7 | "skipLibCheck": true, 8 | "module": "CommonJS", 9 | "strict": true, 10 | "outDir": "dist" 11 | }, 12 | "include": ["src"], 13 | "exclude": ["node_modules", "dist"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/persist-and-sync/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | // eslint-disable-next-line import/no-default-export -- export default is required for config files 6 | export default defineConfig({ 7 | plugins: [react()], 8 | test: { 9 | environment: "jsdom", 10 | globals: true, 11 | setupFiles: ["vitest.setup.ts"], 12 | coverage: { 13 | reporter: ["text", "json", "clover", "html"], 14 | include: ["src/**/*.ts"], 15 | }, 16 | mockReset: false, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /packages/persist-and-sync/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { act } from "@testing-library/react"; 2 | import { afterEach, vi } from "vitest"; 3 | import type { StateCreator } from "zustand"; 4 | import { create as actualCreate } from "zustand"; 5 | 6 | // a variable to hold reset functions for all stores declared in the app 7 | export const storeResetFns = new Set<() => void>(); 8 | 9 | // when creating a store, we get its initial state, create a reset function and add it in the set 10 | export const create = (createState: StateCreator) => { 11 | const store = actualCreate(createState); 12 | const initialState = store.getState(); 13 | storeResetFns.add(() => store.setState(initialState, true)); 14 | return store; 15 | }; 16 | 17 | afterEach(() => { 18 | act(() => storeResetFns.forEach(resetFn => resetFn())); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "strictNullChecks": true 19 | }, 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "plugins": [{ "name": "next" }], 7 | "allowJs": true, 8 | "declaration": false, 9 | "declarationMap": false, 10 | "incremental": true, 11 | "jsx": "preserve", 12 | "lib": ["dom", "dom.iterable", "esnext"], 13 | "module": "esnext", 14 | "noEmit": true, 15 | "resolveJsonModule": true, 16 | "strict": false, 17 | "target": "es5" 18 | }, 19 | "include": ["src", "next-env.d.ts"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "publishConfig": { 7 | "access": "public" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/tsconfig/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "jsx": "react-jsx", 7 | "lib": ["ES2015", "DOM"], 8 | "module": "ESNext", 9 | "target": "es6" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "examples/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json" 3 | } 4 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": [ 4 | "**/.env.*local" 5 | ], 6 | "tasks": { 7 | "build": { 8 | "dependsOn": [ 9 | "^build" 10 | ], 11 | "outputs": [ 12 | ".next/**", 13 | "!.next/cache/**" 14 | ] 15 | }, 16 | "persist-and-sync#build": { 17 | "cache": false 18 | }, 19 | "test": {}, 20 | "lint": {}, 21 | "dev": { 22 | "cache": false, 23 | "persistent": true 24 | } 25 | } 26 | } 27 | --------------------------------------------------------------------------------