Skip to content

Commit

Permalink
✨ feat: partialize / rehydrate
Browse files Browse the repository at this point in the history
Add support for complex states with nested functions and other types.
  • Loading branch information
ketchel authored Dec 12, 2024
1 parent 633d0c8 commit a335a0e
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 47 deletions.
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,22 @@ Type: `boolean` (default: `false`)

If true, the store will only synchronize once with the main tab. After that, the store will be unsynchronized.

##### options.skipSerialization

Type: `boolean` (default `false`)

If true, will not serialize the state with `JSON.parse(JSON.stringify(state))` before sending it. This results in a performance boost, but you will have to ensure there are no unsupported types in the state or it will result in errors. See section [What data can I send?](#what-data-can-i-send) for more info.

##### options.partialize
Type: `(state: T) => Partial<T>` (default: `undefined`)

Similar to `partialize` in the [Zustand persist middleware](https://zustand.docs.pmnd.rs/integrations/persisting-store-data#partialize), allows you to pick which of the state's fields are sent to other tabs. Can also be used to pre-process the state before it's sent if needed.

##### options.merge
Type: `(state: T, receivedState: Partial<T>) => T` (default: `undefined`)

Similar to `merge` in the [Zustand persist middleware](https://zustand.docs.pmnd.rs/integrations/persisting-store-data#merge). A custom function that allows you to merge the current state with the state received from another tab.

##### options.onBecomeMain

Type: `(id: number) => void`
Expand Down Expand Up @@ -224,7 +240,7 @@ Subscribe to the channel. The callback will be called when the channel receive a

## What data can I send?

You can send any of the supported types by the structured clone algorithm like :
You can send any of the supported types by the structured clone algorithm and `JSON.stringify` like :

- `String`
- `Boolean`
Expand All @@ -238,9 +254,10 @@ In short, you cannot send :

- `Function`
- `Dom Element`
- `BigInt` (This is only unsupported by `JSON.stringify`, so if you set `skipSerialization=true`, `BigInt`'s will work)
- And some other types

See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) for more information.
See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) for more information. However, if you need to, you could use `partialize` to convert an unsupported type to a string and convert it back on the other end by providing a `merge` function.

## License

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "use-broadcast-ts",
"version": "1.8.0",
"version": "2.0.0",
"description": "Use the Broadcast Channel API in React easily with hooks or Zustand, and Typescript!",
"type": "module",
"types": "./dist/index.d.ts",
Expand Down
106 changes: 62 additions & 44 deletions src/shared.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { StateCreator, StoreMutatorIdentifier } from 'zustand';

export type SharedOptions = {
export type SharedOptions<T = unknown> = {
/**
* The name of the broadcast channel
* It must be unique
Expand All @@ -20,6 +20,28 @@ export type SharedOptions = {
*/
unsync?: boolean;

/**
* If true, will not serialize with JSON.parse(JSON.stringify(state)) the state before sending it.
* This results in a performance boost, but it is on the user to ensure there are no unsupported types in their state.
* @default false
*/
skipSerialization?: boolean;

/**
* Custom function to parse the state before sending it to the other tabs
* @param state The state
* @returns The parsed state
*/
partialize?: (state: T) => Partial<T>;

/**
* Custom function to merge the state after receiving it from the other tabs
* @param state The current state
* @param receivedState The state received from the other tab
* @returns The restored state
*/
merge?: (state: T, receivedState: Partial<T>) => T;

/**
* Callback when this tab / window becomes the main tab / window
* Triggered only in the main tab / window
Expand All @@ -37,18 +59,18 @@ export type SharedOptions = {
* The Shared type
*/
export type Shared = <
T,
T extends object,
Mps extends [StoreMutatorIdentifier, unknown][] = [],
Mcs extends [StoreMutatorIdentifier, unknown][] = []
>(
f: StateCreator<T, Mps, Mcs>,
options?: SharedOptions
options?: SharedOptions<T>
) => StateCreator<T, Mps, Mcs>;

/**
* Type implementation of the Shared function
*/
type SharedImpl = <T>(f: StateCreator<T, [], []>, options?: SharedOptions) => StateCreator<T, [], []>;
type SharedImpl = <T>(f: StateCreator<T, [], []>, options?: SharedOptions<T>) => StateCreator<T, [], []>;

/**
* Shared implementation
Expand Down Expand Up @@ -79,6 +101,8 @@ const sharedImpl: SharedImpl = (f, options) => (set, get, store) => {
/**
* Types
*/
type T = ReturnType<typeof get>;

type Item = { [key: string]: unknown };
type Message =
| {
Expand Down Expand Up @@ -134,16 +158,37 @@ const sharedImpl: SharedImpl = (f, options) => (set, get, store) => {
*/
const channel = new BroadcastChannel(name);

const sendChangeToOtherTabs = () => {

let state: Item = get() as Item;

/**
* If the partialize function is provided, use it to parse the state
*/
if (options?.partialize) {
// Partialize the state
state = options.partialize(state as T);
}

/**
* If the user did not specify that serialization should be skipped, remove unsupported types
*/
if (!options?.skipSerialization){
// Remove unserializable types (functions, Symbols, etc.) from the state.
state = JSON.parse(JSON.stringify(state))
}

/**
* Send the states to all the other tabs
*/
channel.postMessage({ action: 'change', state } as Message);
}

/**
* Handle the Zustand set function
* Trigger a postMessage to all the other tabs
*/
const onSet: typeof set = (...args) => {
/**
* Get the previous states
*/
const previous = get() as Item;

/**
* Update the states
*/
Expand All @@ -156,25 +201,7 @@ const sharedImpl: SharedImpl = (f, options) => (set, get, store) => {
return;
}

/**
* Get the fresh states
*/
const updated = get() as Item;

/**
* Get the states that changed
*/
const state = Object.entries(updated).reduce((obj, [key, val]) => {
if (previous[key] !== val) {
obj = { ...obj, [key]: val };
}
return obj;
}, {} as Item);

/**
* Send the states to all the other tabs
*/
channel.postMessage({ action: 'change', state } as Message);
sendChangeToOtherTabs();
};

/**
Expand All @@ -188,21 +215,8 @@ const sharedImpl: SharedImpl = (f, options) => (set, get, store) => {
if (!isMain) {
return;
}

/**
* Remove all the functions and symbols from the store
*/
const state = Object.entries(get() as Item).reduce((obj, [key, val]) => {
if (typeof val !== 'function' && typeof val !== 'symbol') {
obj = { ...obj, [key]: val };
}
return obj;
}, {});

/**
* Send the state to the other tabs
*/
channel.postMessage({ action: 'change', state } as Message);

sendChangeToOtherTabs();

/**
* Set the new tab / window id
Expand Down Expand Up @@ -232,7 +246,11 @@ const sharedImpl: SharedImpl = (f, options) => (set, get, store) => {
/**
* Update the state
*/
set(e.data.state);
set((state) => (
options?.merge?
options.merge(state, e.data.state as Partial<T>):
e.data.state
));

/**
* Set the synced attribute
Expand Down

0 comments on commit a335a0e

Please sign in to comment.