Skip to content

Commit 7c1c7e4

Browse files
fix: 🐛 enhance query parameter store for SSR and initialization scenarios
1 parent 3cea07e commit 7c1c7e4

File tree

2 files changed

+173
-2
lines changed

2 files changed

+173
-2
lines changed

src/query-parameter-store.test.tsx

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,4 +626,165 @@ describe('createQueryParamStore', () => {
626626
expect(result2.current[0]).toEqual({ param2: 'value2' })
627627
expect(mockRouter.query).toEqual({ param1: 'value1', param2: 'value2' })
628628
})
629+
630+
it('correctly synchronizes initial SSR state with client state', async () => {
631+
// Arrange
632+
const schemaWithDefault = z.object({
633+
status: z.string().default('all'),
634+
search: z.string().optional(),
635+
})
636+
const { useQueryParams } = createQueryParamStore(schemaWithDefault)
637+
638+
// Simulate SSR by setting initial router state
639+
await mockRouter.push({ query: { status: 'draft' } })
640+
641+
// Act
642+
const { result } = renderHook(() => useQueryParams(), {
643+
wrapper: MemoryRouterProvider,
644+
})
645+
646+
// Assert
647+
await waitFor(() => {
648+
// Changed expectation to match the schema definition
649+
expect(result.current[0]).toEqual({ status: 'draft' })
650+
expect(mockRouter.query.status).toBe('draft')
651+
})
652+
})
653+
654+
it('maintains router query value over default value during initialization', async () => {
655+
// Arrange
656+
const schemaWithDefault = z.object({
657+
status: z.string().default('all'),
658+
})
659+
const { useQueryParams } = createQueryParamStore(schemaWithDefault)
660+
661+
// Set initial router state
662+
await mockRouter.push({ query: { status: 'draft' } })
663+
664+
// Act
665+
const { result } = renderHook(() => useQueryParams(), {
666+
wrapper: MemoryRouterProvider,
667+
})
668+
669+
// Assert
670+
await waitFor(() => {
671+
expect(result.current[0].status).toBe('draft')
672+
})
673+
674+
// Update the value
675+
act(() => {
676+
result.current[1]({ status: 'sent' })
677+
})
678+
679+
// Assert the update worked
680+
await waitFor(() => {
681+
expect(result.current[0].status).toBe('sent')
682+
})
683+
})
684+
685+
it('handles initialization order correctly with multiple sources', async () => {
686+
// Arrange
687+
const customSchema = z.object({
688+
status: z.string().default('all'),
689+
search: z.string().optional(),
690+
})
691+
692+
const { useQueryParams } = createQueryParamStore(customSchema)
693+
694+
// Set up different values from different sources
695+
const initialQuery = { status: 'initial' } as ParsedUrlQuery
696+
await mockRouter.push({ query: { status: 'router' } })
697+
698+
// Act
699+
const { result } = renderHook(() => useQueryParams(initialQuery), {
700+
wrapper: MemoryRouterProvider,
701+
})
702+
703+
// Assert - router query should take precedence
704+
await waitFor(() => {
705+
expect(result.current[0].status).toBe('router')
706+
})
707+
})
708+
709+
it('preserves SSR values during hydration', async () => {
710+
// Arrange
711+
const customSchema = z.object({
712+
status: z.string(),
713+
search: z.string().optional(),
714+
})
715+
const { useQueryParams } = createQueryParamStore(customSchema)
716+
const ssrQuery = { status: 'sent', search: '' }
717+
718+
// Act
719+
const { result } = renderHook(() => useQueryParams(ssrQuery), {
720+
wrapper: MemoryRouterProvider,
721+
})
722+
723+
// Assert
724+
expect(result.current[0]).toEqual(ssrQuery)
725+
726+
// Simulate client-side navigation
727+
await act(async () => {
728+
await mockRouter.push({ query: { status: 'draft' } })
729+
})
730+
731+
// Check that new values are reflected
732+
await waitFor(() => {
733+
expect(result.current[0]).toEqual({ status: 'draft', search: '' })
734+
})
735+
})
736+
737+
it('handles redirect scenarios correctly', async () => {
738+
// Arrange
739+
const customSchema = z.object({
740+
status: z.string(),
741+
page: z.string().optional(),
742+
})
743+
const { useQueryParams } = createQueryParamStore(customSchema)
744+
const initialQuery = { status: 'initial' }
745+
746+
// Act - simulate a redirect scenario
747+
const { result, rerender } = renderHook(() => useQueryParams(initialQuery), {
748+
wrapper: MemoryRouterProvider,
749+
})
750+
751+
// Simulate redirect by changing router query
752+
await act(async () => {
753+
await mockRouter.push({ query: { status: 'redirected', page: '2' } })
754+
})
755+
756+
// Force rerender to simulate redirect completion
757+
rerender()
758+
759+
// Assert
760+
await waitFor(() => {
761+
expect(result.current[0]).toEqual({ status: 'redirected', page: '2' })
762+
})
763+
})
764+
765+
it('maintains query param order during initialization', async () => {
766+
// Arrange
767+
const customSchema = z.object({
768+
status: z.string().default('all'),
769+
search: z.string().optional(),
770+
})
771+
const { useQueryParams } = createQueryParamStore(customSchema)
772+
773+
// Set up competing values
774+
const ssrQuery = { status: 'ssr', search: 'ssr-search' }
775+
await mockRouter.push({ query: { status: 'client', search: 'client-search' } })
776+
777+
// Act
778+
const { result } = renderHook(() => useQueryParams(ssrQuery), {
779+
wrapper: MemoryRouterProvider,
780+
})
781+
782+
// Assert - client values should take precedence
783+
await waitFor(() => {
784+
expect(result.current[0]).toEqual({
785+
status: 'client',
786+
search: 'client-search',
787+
})
788+
})
789+
})
629790
})

src/query-parameter-store.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ const createUseQueryParamStore = <P extends z.ZodType>(
216216
// Use the snapshot from the store
217217
() => queryParamStore.getSnapshot(),
218218
// Server snapshot
219-
() => queryParamStore.getSnapshot(),
219+
() => queryParamStore.getSnapshot(initialQuery),
220220
)
221221

222222
useEffect(() => {
@@ -246,7 +246,17 @@ const createUseQueryParamStore = <P extends z.ZodType>(
246246

247247
if (Object.keys(defaultsNotInUrl).length > 0 || !isStateEqualToRouterQuery) {
248248
// Update state to match parsed router query on first render
249-
const mergedState = { ...initialQueryRef.current, ...router.query, ...state, ...parsedRouterQuery }
249+
// Reorder merging priority:
250+
// 1. zodDefaults (lowest priority)
251+
// 2. initialQueryRef.current
252+
// 3. router.query
253+
// 4. parsedRouterQuery (highest priority - contains validated state)
254+
const mergedState = {
255+
...zodDefaults,
256+
...initialQueryRef.current,
257+
...router.query,
258+
...parsedRouterQuery,
259+
}
250260
queryParamStore.setQueryParams(mergedState, { replace: true, shallow: true })
251261
}
252262

0 commit comments

Comments
 (0)