Git Product home page Git Product logo

react18-tools / persist-and-sync Goto Github PK

View Code? Open in Web Editor NEW
29.0 1.0 2.0 71 KB

Zustand middleware to easily persist and sync Zustand state between tabs / windows / iframes (Same Origin)

Home Page: https://persist-and-sync.vercel.app

License: MIT License

JavaScript 23.35% TypeScript 76.65%
javascript mayank1513 middleware persist-and-sync persistent-storage react react-library react18 react18-tools typescript typescript-library typescript-react zustand

persist-and-sync's Introduction

Persist-And-Sync Zustand Store

test Maintainability codecov Version Downloads npm bundle size

Zustand middleware to easily persist and sync Zustand state between tabs/windows/iframes (Same Origin)

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

  • โœ… ๐Ÿ™ ~ 1 kB size cross-tab state sharing + persistence for zustand
  • โœ… Full TypeScript Support
  • โœ… solid reliability in 1 writing and n reading tab scenarios (with changing writing tab)
  • โœ… Fire and forget approach of always using the latest state. Perfect for single-user systems
  • โœ… Share state between multiple browsing contexts
  • โœ… Additional control over which fields to persist-and-sync and which to ignore
  • โœ… Optimized for performance using memoization and closures.
  • โœ… Update options at runtime by setting __persistNSyncOptions in your store.

Install

$ pnpm add persist-and-sync

or

$ npm install persist-and-sync

or

$ yarn add persist-and-sync

Usage

Add the middleware while creating the store and the rest will be taken care.

import { create } from "zustand";
import { persistNSync } from "persist-and-sync";

type MyStore = {
	count: number;
	set: (n: number) => void;
};

const useStore = create<MyStore>(
	persistNSync(
		set => ({
			count: 0,
			set: n => set({ count: n }),
		}),
		{ name: "my-example" },
	),
);

โšก๐ŸŽ‰Boom! Just a couple of lines and your state perfectly syncs between tabs/windows and it is also persisted using localStorage!

Advanced Usage (Customizations)

PersistNSyncOptions

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.

type PersistNSyncOptionsType = {
	name: string;
	/** @deprecated */
	regExpToIgnore?: RegExp;
	include?: (string | RegExp)[];
	exclude?: (string | RegExp)[];
	storage?: "localStorage" | "sessionStorage" | "cookies" /** Added in v1.1.0 */;
};

Example

export const useMyStore = create<MyStoreType>()(
	persistNSync(
		set => ({
			count: 0,
			_count: 0 /** skipped as it is included in exclude array */,
			setCount: count => {
				set(state => ({ ...state, count }));
			},
			set_Count: _count => {
				set(state => ({ ...state, _count }));
			},
		}),
		{ name: "example", exclude: ["_count"] },
	),
);

It is good to note here that each element of include and exclude array can either be a string or a regular expression. 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. 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.

Updating options at runtime

Since version 1.2, you can also update the options at runTime by setting __persistNSyncOptions in your Zustand state.

Example

interface StoreWithOptions {
	count: number;
	_count: number;
	__persistNSyncOptions: PersistNSyncOptionsType;
	setCount: (c: number) => void;
	set_Count: (c: number) => void;
	setOptions: (__persistNSyncOptions: PersistNSyncOptionsType) => void;
}

const defaultOptions = { name: "example", include: [/count/], exclude: [/^_/] };

export const useStoreWithOptions = create<StoreWithOptions>(
	persistNSync(
		set => ({
			count: 0,
			_count: 0 /** skipped as it matches the regexp provided */,
			__persistNSyncOptions: defaultOptions,
			setCount: count => set(state => ({ ...state, count })),
			set_Count: _count => set(state => ({ ...state, _count })),
			setOptions: __persistNSyncOptions => set(state => ({ ...state, __persistNSyncOptions })),
		}),
		defaultOptions,
	),
);

Clear Storage

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.

import { clearStorage } from "persist-and-sync";

...
	clearStorage("my-store", "cookies");
...

Legacy / Deprecated

Ignore/filter out fields based on regExp

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.

// to ignore fields containing a slug
persistNSync(
    set => ({
      count: 0,
      slugSomeState: 1,
      slugSomeState2: 1,
      set: n => set({ count: n }),
    }),
    { name: "my-channel", regExpToIgnore: /slug/ },
    // or regExpToIgnore: new RegExp('slug')
    // Use full power of regExp by adding `i` and `g` flags
  ),

For more details about regExp check out - JS RegExp

Exact match

For exactly matching a parameter/field use /^your-field-name$/. ^ forces match from the first character and similarly, $ forces match until the last character.

Ignore multiple fields with exact match

use regExpToIgnore: /^(field1|field2|field3)$/

๐Ÿคฉ Don't forget to star this repo!

Want a hands-on course for getting started with Turborepo? Check out React and Next.js with TypeScript and The Game of Chess with Next.js, React and TypeScrypt

License

Licensed as MIT open source.


with ๐Ÿ’– by Mayank Kumar Chaudhari

persist-and-sync's People

Contributors

mayank1513 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

persist-and-sync's Issues

Storage overridden unexpectedly

Hi,

I have an issue where everything was working fine but then, I added a third property to include in the sessionStorage.
Instead of just adding it to the ones already there, anytime this third property is modified, the whole sessionStorage is cleared and only the third property is inside.

Here are my options (I don't show you the store's data because it is HUGE)
image

Here is my sessionStorage before a change occurs to pagesConfig. Notice that only pages and history are present.
image

Here is a screenshot after I change pagesConfig, it just clears the sessionStorage. If now I update pages or history it will basically go back to the previous state with only both of them included in the sessionStorage.
image

I'd simply like to store the 3 properties and at all time, with no override.

Throw Error when store name is changed.

Is your feature request related to a problem? Please describe.
The name of the persistent store can not be changed, as it is used before the state is loaded.

Describe the solution you'd like

  1. Create an interface for __persistNSyncOptions. This interface should not include name.
  2. Throw an error when name is changed.
  3. Always read option.name and not prevState.__persistNSyncOptions.name

Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.

Additional context
Add any other context or screenshots about the feature request here.

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

Does this package only persist one state value from the store?

I'd like to be able to read and modify multiple pieces of state from different contexts, such as tabs, and have any modifications to the store be persisted to storage and synced across all active contexts. I thought that was the purpose of this package, but it does not seem to work for the following setup:

import { create } from "zustand";
import { persistNSync } from "persist-and-sync";

interface MyStoreType {
	count: number;
	count2: number;
	incrementCount: () => void;
	incrementCount2: () => void;
}

export const useMyStore = create<MyStoreType>()(
	persistNSync(
		set => ({
			count: 0,
			count2: 0,
			incrementCount: () => {
				set(state => ({ ...state, count: state.count + 1 }));
			},
			incrementCount2: () => {
				set(state => ({ ...state, count2: state.count2 + 1 }));
			},
		}),
		{ name: "shared_counts", storage: "localStorage" },
	),
);

When one tab calls incrementCount() the state of count2 is cleared from the local store. Thus on refresh of one or both tabs, the store is rehydrated and count2 is 0. Is this expected? Perhaps only one context is able to mutate the stores state?

If so are there any reasonable means of achieving the type of "complete synchronization" of state between tabs where each tab can execute actions upon the state and have the resulting state mutations also apply to the persisted store AND in-memory state of the other contexts using the shared store?

I can make it work if I use something more like:

import { create } from "zustand";
import { persistNSync } from "persist-and-sync";

interface MyStoreType {
	count_state: {
		count: number;
		count2: number;
	};
	incrementCount: () => void;
	incrementCount2: () => void;
}

export const useMyStore = create<MyStoreType>()(
	persistNSync<MyStoreType>(
		(set, get) => ({
			count_state: {
				count: 0,
				count2: 0,
			},
			incrementCount: () =>
				set(state => ({
					count_state: { ...state.count_state, count: state.count_state.count + 1 },
				})),
			incrementCount2: () =>
				set(state => ({
					count_state: { ...state.count_state, count2: state.count_state.count2 + 1 },
				})),
		}),
		{ name: "shared_counts", storage: "localStorage" },
	),
);

But by doing this we lose the ability to use more fine-grained selectors, and need to instead select the entire state during every read / update. Maybe this is unavoidable though? Any thoughts would be appreciated, and If I am misunderstanding how the package (or zustand in general) works, corrections are welcome. I am rather new to Zustand. Thanks.

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.