Skip to content

Commit

Permalink
fix(next): Keep current history context at refresh (#69)
Browse files Browse the repository at this point in the history
* fix

* remove console.log

* changeset
  • Loading branch information
minuukang authored Sep 26, 2024
1 parent 1aa6e98 commit 093ac8b
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 44 deletions.
10 changes: 10 additions & 0 deletions .changeset/tame-adults-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@use-funnel/core': patch
'@use-funnel/next': patch
'@use-funnel/browser': patch
'@use-funnel/react-navigation-native': patch
'@use-funnel/react-router-dom': patch
---

fix(next): Keep current history context at refresh
fix(core): Fix useFunnelRouter using initial optionRef
33 changes: 17 additions & 16 deletions packages/core/src/useFunnel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ export interface UseFunnel<TRouteOption extends RouteOption> {
TStepKeys extends keyof _TStepContextMap = keyof _TStepContextMap,
TStepContext extends _TStepContextMap[TStepKeys] = _TStepContextMap[TStepKeys],
TStepContextMap extends string extends keyof _TStepContextMap
? Record<TStepKeys, TStepContext>
: _TStepContextMap = string extends keyof _TStepContextMap ? Record<TStepKeys, TStepContext> : _TStepContextMap,
? Record<TStepKeys, TStepContext>
: _TStepContextMap = string extends keyof _TStepContextMap ? Record<TStepKeys, TStepContext> : _TStepContextMap,
>(
options: UseFunnelOptions<TStepContextMap>,
): UseFunnelResults<TStepContextMap, TRouteOption>;
Expand All @@ -49,15 +49,16 @@ export function createUseFunnel<TRouteOption extends RouteOption>(
TStepKeys extends keyof _TStepContextMap = keyof _TStepContextMap,
TStepContext extends _TStepContextMap[TStepKeys] = _TStepContextMap[TStepKeys],
TStepContextMap extends string extends keyof _TStepContextMap
? Record<TStepKeys, TStepContext>
: _TStepContextMap = string extends keyof _TStepContextMap ? Record<TStepKeys, TStepContext> : _TStepContextMap,
? Record<TStepKeys, TStepContext>
: _TStepContextMap = string extends keyof _TStepContextMap ? Record<TStepKeys, TStepContext> : _TStepContextMap,
>(options: UseFunnelOptions<TStepContextMap>): UseFunnelResults<TStepContextMap, TRouteOption> {
const optionsRef = useUpdatableRef(options);
const router = useFunnelRouter({
id: options.id,
initialState: options.initial,
id: optionsRef.current.id,
initialState: optionsRef.current.initial,
});
const currentState = (router.history[router.currentIndex] ?? options.initial) as FunnelStateByContextMap<TStepContextMap>;
const currentState = (router.history[router.currentIndex] ??
options.initial) as FunnelStateByContextMap<TStepContextMap>;
const currentStateRef = useUpdatableRef(currentState);

const parseStepContext = useCallback(
Expand Down Expand Up @@ -85,16 +86,16 @@ export function createUseFunnel<TRouteOption extends RouteOption>(
typeof assignContext === 'function'
? assignContext(currentStateRef.current.context)
: {
...currentStateRef.current.context,
...assignContext,
};
...currentStateRef.current.context,
...assignContext,
};
const context = parseStepContext(step, newContext);
return context == null
? optionsRef.current.initial
: ({
step,
context,
} as FunnelStateByContextMap<TStepContextMap>);
step,
context,
} as FunnelStateByContextMap<TStepContextMap>);
};
return {
push: async (...args) => {
Expand All @@ -120,9 +121,9 @@ export function createUseFunnel<TRouteOption extends RouteOption>(
...(validContext == null
? optionsRef.current.initial
: {
step: currentState.step,
context: validContext,
}),
step: currentState.step,
context: validContext,
}),
history,
index: router.currentIndex,
historySteps: router.history as FunnelStateByContextMap<TStepContextMap>[],
Expand Down
54 changes: 38 additions & 16 deletions packages/next/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ export * from '@use-funnel/core';

const QS_KEY = 'funnel.';

const checkHasQSKey = (key: string) => key.startsWith(QS_KEY);
const STEP_KEY = '.step';
const CONTEXT_KEY = '.context';
const HISTORY_KEY = '.histories';

const checkIsHistoryKey = (key: string) => key.startsWith(QS_KEY) && key.endsWith(HISTORY_KEY);

interface NextPageRouteOption {
shallow?: boolean;
Expand All @@ -18,37 +22,52 @@ interface NextPageRouteOption {
export const useFunnel = createUseFunnel<NextPageRouteOption>(({ id, initialState }) => {
const router = useRouter();

const _currentIndex = Number(router.query?.[`${QS_KEY}${id}.index`]);
const currentIndex = isNaN(_currentIndex) ? 0 : _currentIndex;
const currentContext = useMemo(() => {
try {
const currentStep = router.query?.[`${QS_KEY}${id}${STEP_KEY}`] as string | undefined;
const currentContext = router.query?.[`${QS_KEY}${id}${CONTEXT_KEY}`] as string | undefined;
return currentStep == null || currentContext == null
? initialState
: { step: currentStep, context: JSON.parse(currentContext) };
} catch {
return initialState;
}
}, [router.query, initialState]);

const _histories = router.query?.[`${QS_KEY}${id}.histories`];
const histories = useMemo<(typeof initialState)[]>(() => {
const _beforeHistories = router.query?.[`${QS_KEY}${id}${HISTORY_KEY}`];
const beforeHistories = useMemo<(typeof initialState)[]>(() => {
try {
return _histories == null ? [initialState] : JSON.parse(_histories as string);
return _beforeHistories == null ? [] : JSON.parse(_beforeHistories as string);
} catch {
return [initialState];
return [];
}
}, [_histories]);
}, [_beforeHistories]);

const currentIndex = history.length;

return useMemo(
() => ({
history: histories,
history: [...beforeHistories, currentContext],
currentIndex,
async push(state, { scroll, locale, shallow = true } = {}) {
const { pathname, query } = makePath(router);
const queryContext = {
[`${QS_KEY}${id}${STEP_KEY}`]: state.step,
[`${QS_KEY}${id}${CONTEXT_KEY}`]: JSON.stringify(state.context),
};

await router.push(
{
pathname,
query: {
...query,
[`${QS_KEY}${id}.histories`]: JSON.stringify([...histories, state]),
[`${QS_KEY}${id}.index`]: currentIndex + 1,
[`${QS_KEY}${id}${HISTORY_KEY}`]: JSON.stringify([...beforeHistories, currentContext]),
...queryContext,
},
},
{
pathname,
query: { ...removeKeys(query, [checkHasQSKey]), [`${QS_KEY}${id}.index`]: currentIndex + 1 },
query: { ...removeKeys(query, [checkIsHistoryKey]), ...queryContext },
},
{
shallow,
Expand All @@ -59,19 +78,22 @@ export const useFunnel = createUseFunnel<NextPageRouteOption>(({ id, initialStat
},
async replace(state, { scroll, locale, shallow = true } = {}) {
const { pathname, query } = makePath(router);
const queryContext = {
[`${QS_KEY}${id}${STEP_KEY}`]: state.step,
[`${QS_KEY}${id}${CONTEXT_KEY}`]: JSON.stringify(state.context),
};

await router.replace(
{
pathname,
query: {
...query,
[`${QS_KEY}${id}.histories`]: JSON.stringify([...(histories ?? []).slice(0, currentIndex), state]),
[`${QS_KEY}${id}.index`]: currentIndex,
...queryContext,
},
},
{
pathname,
query: { ...removeKeys(query, [checkHasQSKey]), [`${QS_KEY}${id}.index`]: currentIndex },
query: { ...removeKeys(query, [checkIsHistoryKey]), ...queryContext },
},
{
shallow,
Expand All @@ -82,6 +104,6 @@ export const useFunnel = createUseFunnel<NextPageRouteOption>(({ id, initialStat
},
go: (index) => window.history.go(index),
}),
[router, histories],
[router, currentIndex, beforeHistories, currentContext],
);
});
20 changes: 8 additions & 12 deletions packages/next/test/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { describe, expect, test } from 'vitest';
import mockRouter from 'next-router-mock';

import { describe, expect, test } from 'vitest';
import { useFunnel } from '../src/index.js';

describe('Test useFunnel next router', () => {
Expand Down Expand Up @@ -67,19 +66,16 @@ describe('Test useFunnel next router', () => {
await user.click(screen.getByText('Go B'));

expect(screen.queryByText('vitest')).not.toBeNull();
expect(mockRouter.query['funnel.vitest.index']).toBe(1);
expect(JSON.parse(mockRouter.query['funnel.vitest.histories'] as string)).toEqual([
{ step: 'A', context: {} },
{ step: 'B', context: { id: 'vitest' } },
]);
expect(mockRouter.query['funnel.vitest.step']).toBe('B');
expect(JSON.parse(mockRouter.query['funnel.vitest.context'] as string)).toEqual({
id: 'vitest',
});
expect(JSON.parse(mockRouter.query['funnel.vitest.histories'] as string)).toEqual([{ step: 'A', context: {} }]);

await user.click(screen.getByText('Go C'));

expect(screen.queryByText('Finished!')).not.toBeNull();
expect(mockRouter.query['funnel.vitest.index']).toBe(1);
expect(JSON.parse(mockRouter.query['funnel.vitest.histories'] as string)).toEqual([
{ step: 'A', context: {} },
{ step: 'C', context: { id: 'vitest', password: 'vitest1234' } },
]);
expect(mockRouter.query['funnel.vitest.step']).toBe('C');
expect(JSON.parse(mockRouter.query['funnel.vitest.histories'] as string)).toEqual([{ step: 'A', context: {} }]);
});
});

0 comments on commit 093ac8b

Please sign in to comment.