Skip to content

Commit

Permalink
feat: forward possible promises to push/replace state functions; Make…
Browse files Browse the repository at this point in the history
… nextjs push/replace customizable
  • Loading branch information
BowlingX committed May 31, 2022
1 parent cec4260 commit 4ce8d6f
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 101 deletions.
13 changes: 9 additions & 4 deletions src/lib/adapters/historyjs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,30 @@ export interface Refs {
readonly updateFromQuery: (query: string) => void
}

const createSearch = (query: Record<string, string>) => {
const queryString = new URLSearchParams(query).toString()
return queryString === '' ? '' : `?${queryString}`
}

export const GeschichteWithHistory = forwardRef<Refs, Props>(
({ children, history }, ref) => {
const historyInstance: HistoryManagement = useMemo(() => {
return {
initialSearch: () => history.location.search,
push: (next: string) => {
push: async (query: Record<string, string>) => {
history.push(
{
hash: history.location.hash,
search: next,
search: createSearch(query),
},
{ __g__: true }
)
},
replace: (next: string) =>
replace: async (query: Record<string, string>) =>
history.replace(
{
hash: history.location.hash,
search: next,
search: createSearch(query),
},
{ __g__: true }
),
Expand Down
51 changes: 41 additions & 10 deletions src/lib/adapters/nextjs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import shallow from 'zustand/shallow'
import { StoreState } from '../../middleware'
import { HistoryManagement, StoreContext, useGeschichte } from '../../store'
import type { UrlObject } from 'url'

const split = (url?: string) => url?.split('?') || []

Expand All @@ -27,11 +28,25 @@ interface TransitionOptions {
readonly scroll?: boolean
}

declare type Url = UrlObject | string

interface Props {
readonly initialClientOnlyAsPath?: string
readonly asPath: string
readonly defaultPushOptions?: TransitionOptions
readonly defaultReplaceOptions?: TransitionOptions
// tslint:disable-next-line:no-mixed-interface
readonly routerPush?: (
url: Url,
as: UrlObject,
options?: TransitionOptions
) => Promise<boolean>
// tslint:disable-next-line:no-mixed-interface
readonly routerReplace?: (
url: Url,
as: UrlObject,
options?: TransitionOptions
) => Promise<boolean>
}

// FIXME: Somehow imports are messed up for nextjs when importing from modules (see https://github.com/vercel/next.js/issues/36794)
Expand All @@ -43,6 +58,8 @@ export const GeschichteForNextjs: FC<Props> = ({
initialClientOnlyAsPath,
defaultPushOptions,
defaultReplaceOptions,
routerPush,
routerReplace,
}) => {
const lastClientSideQuery = useRef(initialClientOnlyAsPath)
const historyInstance: HistoryManagement = useMemo(() => {
Expand All @@ -54,23 +71,34 @@ export const GeschichteForNextjs: FC<Props> = ({
: split(lastClientSideQuery.current || Router.asPath)
return `?${query || ''}`
},
push: (next: string, options) => {
const [path] = split(Router.asPath)
Router.push(Router.route, `${path}${next}`, {
push: (query, options) => {
const [pathname] = split(Router.asPath)
const routerOptions = {
...defaultPushOptions,
...options,
})
}

if (routerPush) {
return routerPush(Router.route, { pathname, query }, routerOptions)
}
return Router.push(Router.route, { pathname, query }, routerOptions)
},
replace: (next: string, options) => {
const [path] = split(Router.asPath)
Router.replace(Router.route, `${path}${next}`, {
replace: (query, options) => {
const [pathname] = split(Router.asPath)

const routerOptions = {
...defaultReplaceOptions,
...options,
})
}

if (routerReplace) {
return routerReplace(Router.route, { pathname, query }, routerOptions)
}
return Router.replace(Router.route, { pathname, query }, routerOptions)
},
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
}, [routerPush, routerReplace])

const useStore = useMemo(
() => useGeschichte(historyInstance),
Expand Down Expand Up @@ -110,7 +138,10 @@ export const GeschichteForNextjs: FC<Props> = ({

type ClientOnlyProps = Pick<
Props,
'defaultPushOptions' | 'defaultReplaceOptions'
| 'defaultPushOptions'
| 'defaultReplaceOptions'
| 'routerPush'
| 'routerReplace'
> & {
readonly children: ReactNode
readonly omitQueries?: boolean
Expand Down
181 changes: 96 additions & 85 deletions src/lib/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ export type PushStateFunction<T> = (
ns: string,
valueCreator: (state: T) => void,
routerOptions?: RouterOptions
) => void
) => Promise<unknown>
export type ReplaceStateFunction<T> = (
ns: string,
valueCreator: (state: T) => void,
routerOptions?: RouterOptions
) => void
) => Promise<unknown>

export interface InnerNamespace<T extends object> {
[ns: string]: NamespaceValues<T>
Expand All @@ -56,12 +56,12 @@ export interface StoreState<ValueState extends object> extends State {
ns: readonly string[],
fn: (...valueState: ValueState[]) => void,
routerOptions?: RouterOptions
) => void
) => Promise<unknown>
readonly batchPushState: (
ns: readonly string[],
fn: (...valueState: ValueState[]) => void,
routerOptions?: RouterOptions
) => void
) => Promise<unknown>
namespaces: InnerNamespace<ValueState>
readonly pushState: PushStateFunction<ValueState>
readonly replaceState: ReplaceStateFunction<ValueState>
Expand Down Expand Up @@ -93,13 +93,13 @@ export type NamespaceProducer<T extends object> = (
eventType: HistoryEventType,
ns?: string,
routerOptions?: RouterOptions
) => void
) => Promise<unknown>
export type GenericConverter<T extends object> = (
stateProducer: InnerNamespaceProducerFunction<T>,
eventType: HistoryEventType,
ns?: string,
routerOptions?: RouterOptions
) => void
) => Promise<unknown>

export type ImmerProducer<T extends object> = (
stateMapper: (changes: Patch[], values: StoreState<T>) => StoreState<T>,
Expand Down Expand Up @@ -129,94 +129,105 @@ export const historyManagement =
ns?: string,
options?: RouterOptions
) => {
// we call the `immerWithPatches` middleware
return set(
(changes: Patch[], values: StoreState<T>) => {
if (changes.length === 0) {
return values
}
if (type !== HistoryEventType.REGISTER) {
// if namespace is not given, calculate what namespaces are affected
const affectedNamespaces: string[] = ns
? [ns]
: changes.reduce((next: string[], change: Patch) => {
const {
path: [, namespace],
} = change

if (next.indexOf(namespace as string) === -1) {
return [...next, namespace as string]
}
return next
}, [])

const uniqueQueries: {
[key: string]: any
} = affectedNamespaces.reduce((next, thisNs) => {
const { config, query: currentQuery } = get().namespaces[thisNs]
return {
...next,
[thisNs]: applyDiffWithCreateQueriesFromPatch(
config,
thisNs,
currentQuery,
changes,
values.namespaces[thisNs].values,
values.namespaces[thisNs].initialValues
),
}
}, {})
return new Promise<unknown>((resolve, reject) => {
set(
(changes: Patch[], values: StoreState<T>) => {
if (changes.length === 0) {
resolve(null)
return values
}
if (type !== HistoryEventType.REGISTER) {
// if namespace is not given, calculate what namespaces are affected
const affectedNamespaces: string[] = ns
? [ns]
: changes.reduce((next: string[], change: Patch) => {
const {
path: [, namespace],
} = change

const method = type === HistoryEventType.PUSH ? 'push' : 'replace'
if (next.indexOf(namespace as string) === -1) {
return [...next, namespace as string]
}
return next
}, [])

const otherQueries = Object.keys(values.namespaces).reduce(
(next, thisNs) => {
if (affectedNamespaces.indexOf(thisNs) !== -1) {
return next
}
const uniqueQueries: {
[key: string]: any
} = affectedNamespaces.reduce((next, thisNs) => {
const { config, query: currentQuery } =
get().namespaces[thisNs]
return {
...next,
...values.namespaces[thisNs].query,
[thisNs]: applyDiffWithCreateQueriesFromPatch(
config,
thisNs,
currentQuery,
changes,
values.namespaces[thisNs].values,
values.namespaces[thisNs].initialValues
),
}
},
{}
)
}, {})

const reducedQueries = Object.keys(uniqueQueries).reduce(
(next, thisNs) => ({ ...next, ...uniqueQueries[thisNs] }),
{}
)

const query = new URLSearchParams({
...otherQueries,
...reducedQueries,
}).toString()
historyInstance[method](query === '' ? '' : `?${query}`, options)
const method =
type === HistoryEventType.PUSH ? 'push' : 'replace'

// We safe the current state of `query` for all affected namespaces
return {
...values,
namespaces: {
...values.namespaces,
...affectedNamespaces.reduce((next: any, thisNs: string) => {
const otherQueries = Object.keys(values.namespaces).reduce(
(next, thisNs) => {
if (affectedNamespaces.indexOf(thisNs) !== -1) {
return next
}
return {
...next,
[thisNs]: {
...values.namespaces[thisNs],
query: uniqueQueries[thisNs],
},
...values.namespaces[thisNs].query,
}
}, {}),
},
},
{}
)

const reducedQueries = Object.keys(uniqueQueries).reduce(
(next, thisNs) => ({ ...next, ...uniqueQueries[thisNs] }),
{}
)

const queryObject = Object.freeze({
...otherQueries,
...reducedQueries,
})

historyInstance[method](queryObject, options)
.then(resolve)
.catch(reject)

// We save the current state of `query` for all affected namespaces
return {
...values,
namespaces: {
...values.namespaces,
...affectedNamespaces.reduce(
(next: any, thisNs: string) => {
return {
...next,
[thisNs]: {
...values.namespaces[thisNs],
query: uniqueQueries[thisNs],
},
}
},
{}
),
},
}
}
}
return values
},
fn as NamespaceProducerFunction<T> &
InnerNamespaceProducerFunction<T>,
type,
ns
)
resolve(null)
return values
},
fn as NamespaceProducerFunction<T> &
InnerNamespaceProducerFunction<T>,
type,
ns
)
})
// H
},
get,
Expand Down Expand Up @@ -338,7 +349,7 @@ export const converter =
fn: (...valueState: T[]) => void,
routerOptions
) => {
set(
return set(
(state: InnerNamespace<T>) =>
void fn(...ns.map((thisNs) => (state[thisNs] || {}).values)),
HistoryEventType.PUSH,
Expand All @@ -352,7 +363,7 @@ export const converter =
fn: (...valueState: T[]) => void,
routerOptions
) => {
set(
return set(
(state: InnerNamespace<T>) =>
void fn(...ns.map((thisNs) => (state[thisNs] || {}).values)),
HistoryEventType.REPLACE,
Expand Down
10 changes: 8 additions & 2 deletions src/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,14 @@ export type RouterOptions = Record<string, any>
export interface HistoryManagement {
/** the initial search string (e.g. ?query=test), contains the questionsmark */
readonly initialSearch: () => string
readonly push: (next: string, options?: RouterOptions) => void
readonly replace: (next: string, options?: RouterOptions) => void
readonly push: (
queryObject: Record<string, string>,
options?: RouterOptions
) => Promise<unknown>
readonly replace: (
queryObject: Record<string, string>,
options?: RouterOptions
) => Promise<unknown>
}

export const useGeschichte = <T extends State>(
Expand Down

0 comments on commit 4ce8d6f

Please sign in to comment.