react-use-url-state
Version:
React hook for managing state in the URL
274 lines (268 loc) • 7.69 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// packages/core/src/index.ts
var index_exports = {};
__export(index_exports, {
useUrlState: () => useUrlState
});
module.exports = __toCommonJS(index_exports);
var import_react3 = require("react");
// packages/core/src/utils.ts
var import_react = require("react");
function isNil(value) {
return value === null || value === void 0;
}
function urlParamsToObject(params) {
const object = {};
for (const [key, value] of params.entries()) {
if (isNil(object[key])) {
object[key] = value;
continue;
}
const currentValue = object[key];
if (Array.isArray(currentValue)) {
currentValue.push(value);
} else {
object[key] = [currentValue, value];
}
}
return object;
}
function shallowEqual(a, b) {
if (a === b) {
return true;
}
if (typeof a !== "object" || typeof b !== "object") {
return false;
}
if (a === null || b === null) {
return false;
}
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) {
return false;
}
for (const key of aKeys) {
if ("toString" in a && "toString" in b && typeof a.toString === "function" && typeof b.toString === "function") {
if (a.toString() !== b.toString()) {
return false;
}
} else if (a[key] !== b[key]) {
return false;
}
}
return true;
}
function useStableSchema(schema) {
const ref = (0, import_react.useRef)(schema);
if (ref.current === schema) {
return ref.current;
}
if (!shallowEqual(ref.current.shape, schema.shape)) {
ref.current = schema;
}
return ref.current;
}
function serializeValue(value) {
if (isNil(value)) {
return void 0;
}
if (typeof value === "object") {
if (value instanceof Date) {
return value.toISOString();
} else if (Array.isArray(value)) {
return value.map((v) => {
if (typeof v === "object") {
return JSON.stringify(v);
}
return serializeValue(v);
});
} else {
return value?.toString?.();
}
}
return value?.toString();
}
function serializeObjectToUrlParams(object) {
const params = new URLSearchParams();
Object.keys(object).forEach((key) => {
const value = serializeValue(object[key]);
if (value === void 0 || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => {
if (isNil(v)) {
return;
}
params.append(key, v);
});
return;
}
params.append(key, value);
});
return `?${params.toString()}`;
}
// packages/core/src/controller.ts
var UrlStateController = class _UrlStateController {
constructor(options) {
this.options = options;
this.stateString = "";
this.previousHref = "";
this.interval = 0;
this.subscribers = /* @__PURE__ */ new Map();
this.options = { poolingIntervalMs: 100, ...options };
}
static {
this.singleton = null;
}
static getUrlStateController() {
if (!_UrlStateController.singleton) {
_UrlStateController.singleton = new _UrlStateController({});
}
return _UrlStateController.singleton;
}
push(href) {
window.history.pushState({}, "", href);
setTimeout(() => {
this.onSearchParamsChange();
}, 0);
}
subscribe(fn) {
this.subscribers.set(fn, fn);
const newSearchParams = new URLSearchParams(this.stateString);
const search = urlParamsToObject(newSearchParams);
setTimeout(() => fn(search), 0);
if (!this.interval) {
this.startPolling();
}
}
unsubscribe(fn) {
this.subscribers.delete(fn);
if (this.subscribers.size === 0) {
this.stopPolling();
}
}
onSearchParamsChange() {
if (window.location.href !== this.previousHref) {
this.previousHref = window.location.href;
const newSearchParams = new URLSearchParams(window.location.search);
const search = urlParamsToObject(newSearchParams);
this.subscribers.forEach((subscriber) => subscriber(search));
}
}
startPolling() {
if (typeof window !== "undefined") {
this.interval = setInterval(() => {
this.onSearchParamsChange();
}, this.options.poolingIntervalMs);
}
}
stopPolling() {
if (this.interval) {
clearInterval(this.interval);
this.interval = 0;
}
}
};
// packages/core/src/handlers.ts
var import_react2 = require("react");
function useHandlers(controller, stateRef) {
const setState = (0, import_react2.useCallback)(
(state) => {
if (typeof state === "function") {
state = state(stateRef.current.data);
}
const href = serializeObjectToUrlParams(state);
controller.push(href);
},
[controller, stateRef]
);
const setValues = (0, import_react2.useCallback)(
(state) => {
if (typeof state === "function") {
state = state(stateRef.current.data);
}
const href = serializeObjectToUrlParams({
...stateRef.current.data,
...state
});
controller.push(href);
},
[controller, stateRef]
);
const setValue = (0, import_react2.useCallback)(
(key, value) => {
const href = serializeObjectToUrlParams({
...stateRef.current.data,
[key]: value
});
controller.push(href);
},
[controller, stateRef]
);
return (0, import_react2.useMemo)(
() => ({ setState, setValue, setValues }),
[setState, setValue, setValues]
);
}
// packages/core/src/index.ts
function useUrlState(schema, options) {
schema = useStableSchema(schema);
const controller = UrlStateController.getUrlStateController();
const [state, setState] = (0, import_react3.useState)({
data: null,
error: null,
isError: false,
isReady: false
});
const stateRef = (0, import_react3.useRef)(state);
stateRef.current = state;
const recalculateState = (0, import_react3.useCallback)(
(params) => {
const validationResult = schema.safeParse(params);
const result = validationResult.success ? { success: true, data: validationResult.data, error: null } : { success: false, data: null, error: validationResult.error };
setState({
data: result.data,
isError: !result.success,
error: result.error,
isReady: true
});
},
[schema]
);
(0, import_react3.useEffect)(() => {
controller.subscribe(recalculateState);
return () => {
controller.unsubscribe(recalculateState);
};
}, [controller, recalculateState]);
const handlers = useHandlers(controller, stateRef);
(0, import_react3.useEffect)(() => {
if (state.isReady && options?.applyInitialValue) {
handlers.setValues(state.data || {});
}
}, [handlers, options?.applyInitialValue, state.isReady]);
return { ...state, ...handlers };
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
useUrlState
});
;