Skip to content

Instantly share code, notes, and snippets.

@Jonghakseo
Created November 2, 2024 07:17
Show Gist options
  • Save Jonghakseo/0f708ee89b0b8b917623b8cd4cd81f88 to your computer and use it in GitHub Desktop.
Save Jonghakseo/0f708ee89b0b8b917623b8cd4cd81f88 to your computer and use it in GitHub Desktop.
Zustand sync with querystring (for SPA)
import type { StoreApi } from 'zustand/vanilla';
type RemoveFunctionFromObject<T> = {
[key in keyof T as T[key] extends Function ? never : key]: T[key];
};
type Config<Value> = {
initialValue: Value;
serialize?: (value: Value) => string;
deserialize?: (value: string) => Value;
};
type StateFromAPI<API extends StoreApi<any>> = API extends StoreApi<infer State> ? State : never;
export const syncStoreWithURL = <
API extends StoreApi<any>,
State extends StateFromAPI<API>,
SyncTarget extends {
[key in keyof Partial<RemoveFunctionFromObject<State>>]: Config<State[key]>;
},
>(
api: API,
syncTarget: SyncTarget,
): {
[key in keyof State & keyof SyncTarget]: State[key];
} => {
api.subscribe((state, prevState) => {
const url = getCurrentURL();
Object.entries(syncTarget).forEach(([name, config]) => {
const key = name as keyof State & string;
const { initialValue, serialize = defaultSerialize } = config as Config<State[keyof State]>;
const needUpdate = serialize(state[key]) !== serialize(prevState[key]);
if (needUpdate) {
if (serialize(state[key]) === serialize(initialValue)) {
url.searchParams.delete(key);
} else {
url.searchParams.set(key, serialize(state[key]));
}
}
});
history.replaceState(history.state, '', url);
});
return Object.entries(syncTarget).reduce((acc, cur) => {
const [name, config] = cur as [keyof State & string, Config<State[keyof State]>];
const { initialValue, deserialize = defaultDeserialize } = config;
const getValue = () => {
const value = getCurrentURL().searchParams.get(name);
if (value) {
return deserialize(value);
}
return initialValue;
};
return { ...acc, [name]: getValue() };
}, {} as { [key in keyof State & keyof SyncTarget]: State[key] });
};
function getCurrentURL() {
return new URL(window.location.href);
}
function defaultSerialize(value: any) {
switch (value) {
case undefined:
case null:
case '':
return '';
default:
return `${value}`;
}
}
function defaultDeserialize(value: string) {
return value;
}
@Jonghakseo
Copy link
Author

Usage

export const useSomeStore = create((set, _, api) => ({
  ...syncStoreWithURL(api, {
    page: { initialValue: 1 }, // When you want to sync some value with querystring
  }),
  setPage: (page) => set({ page }),  // Auto sync after value changed
  searchKeyword: undefined,  // When you don't want to sync some value with querystring
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment