Skip to content

Instantly share code, notes, and snippets.

@sgb-io
Last active October 31, 2021 20:42
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sgb-io/c1b78a8a411f016342562aae68988b98 to your computer and use it in GitHub Desktop.
Save sgb-io/c1b78a8a411f016342562aae68988b98 to your computer and use it in GitHub Desktop.
Recoil URL persistence example
import type { AtomEffect } from "recoil";
import qs from "query-string";
import { atomDefaults } from "./defaults"; // Contain some default values e.g. { foo: 12, bar: 'hello' }
// In this example, we opt to store some state in the URL as raw params, and the rest as a base64 encoded string in the special `state` param
// e.g. http://localhost:3000?foo=12&bar=hello&state=<base 64 string of some other values>
const IS_BROWSER = typeof window !== "undefined";
const getWindowSearchLocation = () => {
return IS_BROWSER ? window.location.search : "";
};
const decodeBase64 = (input: string): string => {
return IS_BROWSER
? window.atob(input)
: Buffer.from(input, "base64").toString();
};
const encodeBase64 = (input: string): string => {
return IS_BROWSER
? window.btoa(input)
: Buffer.from(input).toString("base64");
};
const getParamStateFromUrl = () => {
const rawParams = qs.parse(getWindowSearchLocation(), {
parseBooleans: true,
parseNumbers: true,
});
// Exclude `state`
const { state, ...result } = rawParams;
return result;
};
const getBase64StateFromUrl = () => {
const rawParams = qs.parse(getWindowSearchLocation(), {
parseBooleans: true,
parseNumbers: true,
});
const { state } = rawParams;
try {
return JSON.parse(decodeBase64(state as string));
} catch (e) {
return {};
}
};
// NOTE: This currently assumes that keys within each atom do not conflict
const sendStateToUrl = (newState: Record<string, any>) => {
const existingState = {
...getParamStateFromUrl(),
state: { ...getBase64StateFromUrl() },
};
const newBase64State = JSON.stringify({
...existingState.state,
...newState.state,
});
const updatedStateWithoutBase64 = {
...existingState,
...newState,
};
const { state, ...existingParamState } = updatedStateWithoutBase64;
const updatedState = {
...existingParamState,
state: encodeBase64(newBase64State),
};
if (IS_BROWSER) {
window.history.replaceState(
updatedState,
window.document.title,
`?${qs.stringify(updatedState)}`
);
}
};
// Note: this currently assumes that no keys will clash between atoms
export const persistInputToUrlParams: AtomEffect<any> = ({
node,
setSelf,
onSet,
trigger,
}): void => {
// On initial load, apply any existing URL state to the Atom
if (trigger === "get") {
const existingState: Record<string, any> = {
...getParamStateFromUrl(),
state: { ...getBase64StateFromUrl() },
};
// Apply relevant existing URL state to this atom
let startingState = {};
startingState = Object.keys(atomDefaults[node.key]).reduce<
Record<string, any>
>((derivedState, stateKey) => {
const existingItem = existingState[stateKey];
if (
!existingItem ||
(typeof existingItem === "object" &&
Object.keys(existingItem).length === 0)
) {
// Falsy or empty object - apply defaults
derivedState[stateKey] = atomDefaults[node.key][stateKey];
} else {
// Existing state found - use it
derivedState[stateKey] = existingState[stateKey];
}
return derivedState;
}, {});
// Now that we have a full starting state, apply it
setSelf(startingState);
// Also re-write to the URL which ensures defaults that needed to be applied are reflected there, too
sendStateToUrl(startingState);
}
// When changes are made to the Atom, send all properties into the URL
onSet((newVal) => {
sendStateToUrl(newVal);
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment