Last active
October 31, 2021 20:42
-
-
Save sgb-io/c1b78a8a411f016342562aae68988b98 to your computer and use it in GitHub Desktop.
Recoil URL persistence example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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