Skip to content

Instantly share code, notes, and snippets.

@tannerlinsley
Last active June 12, 2023 10:19
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save tannerlinsley/0ffe9dbf87a6e1dcb88e529a1941c7e5 to your computer and use it in GitHub Desktop.
Save tannerlinsley/0ffe9dbf87a6e1dcb88e529a1941c7e5 to your computer and use it in GitHub Desktop.

This middleware does a few interesting things:

  • Ensures a url shape in the zustand store, where we'll store URL information.
  • Assumes we will be storing our url state slice in the ?state search parameter after it has been stringified and base 64 encoded.
  • On creation, decodes stores state from the ?state search parameter into the url slice of our store.
  • After each state update, updates the ?state search parameter with the new url state slice.
  • Sets up an event listener that listens for popstate and re-decodes the state from the URL into our store.
import { create, type StateCreator } from 'zustand'
import { immer } from 'zustand/middleware/immer'
function functionalUpdate (updater, previous) {
return typeof updater === 'function' ? updater(previous) : updater
}
export type Store = {
url: {
page: Page
context: PageContext
}
setPage: (page: Page, context?: PageContext) => void
createProject: () => void
}
export type PageContext = {
projectId?: string
}
export type Page = 'projects' | 'projects.project'
const urlMiddleware =
<TState extends { url: any }>(
prev: StateCreator<TState, any, any>
): StateCreator<TState> =>
(set, get, api) => {
const parseUrlState = () => {
try {
const search =
new URLSearchParams(window.location.search.substring(1)).get(
'state'
) || ''
const decoded = atob(search)
return JSON.parse(decoded)
} catch (e) {
return
}
}
const initialState = prev(
(...args) => {
set(...args)
const stringified = JSON.stringify(get().url)
const encoded = btoa(stringified)
history.pushState(null, '', `/?state=${encoded}`)
},
get,
api
)
window.addEventListener('popstate', () => {
set((state) => {
return {
...state,
url: parseUrlState(),
}
})
})
return {
...initialState,
url: parseUrlState() || initialState.url,
}
}
export const useStore = create(
urlMiddleware(
immer<Store>((set) => {
return {
url: {
page: 'projects',
context: {},
},
setPage: (page, context) =>
set((draft) => {
draft.url.page = page
if (context) {
draft.url.context = functionalUpdate(context, draft.url.context)
}
}),
createProject: () => {
set((draft) => {
draft.url.page = 'projects.project'
draft.url.context.projectId = undefined
})
},
}
})
)
)
@EnderKilledYou
Copy link

Neato.

@almilo
Copy link

almilo commented May 16, 2023

Be aware that the base64 encoding can throw an exception in case the string is not properly encoded (eg: URL tampering).

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