From 92edcc307e8c4a5aa720e5cb243d6cd68c0edea2 Mon Sep 17 00:00:00 2001 From: Jonghyeon Ko Date: Fri, 3 Apr 2026 20:07:11 +0900 Subject: [PATCH 1/3] refactor(react): Simplify onError and onSuccess handlers in lazy loading --- .../src/content/en/docs/react/lazy.mdx | 4 +- .../src/content/ko/docs/react/lazy.mdx | 4 +- packages/react/src/lazy.spec.tsx | 35 ++++++++++---- packages/react/src/lazy.ts | 47 +++++++++++++------ 4 files changed, 63 insertions(+), 27 deletions(-) diff --git a/docs/suspensive.org/src/content/en/docs/react/lazy.mdx b/docs/suspensive.org/src/content/en/docs/react/lazy.mdx index 4c9ffe8e2..109830795 100644 --- a/docs/suspensive.org/src/content/en/docs/react/lazy.mdx +++ b/docs/suspensive.org/src/content/en/docs/react/lazy.mdx @@ -127,7 +127,7 @@ const VersionSkewSafeComponent = lazy( + retryDelay: 1000, + })) - const Component = lazy(() => import('./Component'), { -- onError: ({ error, load }) => { +- onError: ({ error }) => { - const reloadKey = 'component_reload_count' - const currentCount = parseInt(sessionStorage.getItem(reloadKey) || '0') - const maxRetries = 3 @@ -141,7 +141,7 @@ const VersionSkewSafeComponent = lazy( - }, 1000) - } - }, -- onSuccess: ({ load }) => { +- onSuccess: () => { - // Clear reload count on success - sessionStorage.removeItem('component_reload_count') - }, diff --git a/docs/suspensive.org/src/content/ko/docs/react/lazy.mdx b/docs/suspensive.org/src/content/ko/docs/react/lazy.mdx index 10f4d1c9f..d436922b5 100644 --- a/docs/suspensive.org/src/content/ko/docs/react/lazy.mdx +++ b/docs/suspensive.org/src/content/ko/docs/react/lazy.mdx @@ -127,7 +127,7 @@ const VersionSkewSafeComponent = lazy( + retryDelay: 1000, + })) - const Component = lazy(() => import('./Component'), { -- onError: ({ error, load }) => { +- onError: ({ error }) => { - const reloadKey = 'component_reload_count' - const currentCount = parseInt(sessionStorage.getItem(reloadKey) || '0') - const maxRetries = 3 @@ -141,7 +141,7 @@ const VersionSkewSafeComponent = lazy( - }, 1000) - } - }, -- onSuccess: ({ load }) => { +- onSuccess: () => { - // 성공 시 재시도 횟수 초기화 - sessionStorage.removeItem('component_reload_count') - }, diff --git a/packages/react/src/lazy.spec.tsx b/packages/react/src/lazy.spec.tsx index 5e6b30de7..f1ead2d35 100644 --- a/packages/react/src/lazy.spec.tsx +++ b/packages/react/src/lazy.spec.tsx @@ -3,7 +3,7 @@ import { act, render, screen } from '@testing-library/react' import { type ComponentType, Suspense, useState } from 'react' import { afterEach, describe, expect, it, vi } from 'vitest' import { ErrorBoundary } from './ErrorBoundary' -import { createLazy, lazy, reloadOnError } from './lazy' +import { createLazy, lazy, reloadOnError, reloadOnErrorStorageKeyAccessKeySymbol } from './lazy' import { sleep } from './test-utils' type PathData = { @@ -317,7 +317,10 @@ describe('lazy', () => { expect(screen.getByText('error')).toBeInTheDocument() expect(onError).toHaveBeenCalledTimes(1) - expect(onError).toHaveBeenCalledWith({ error: expect.any(Error), load: expect.any(Function) }) + expect(onError).toHaveBeenCalledWith({ + error: expect.any(Error), + [reloadOnErrorStorageKeyAccessKeySymbol]: expect.any(String), + }) }) it('should execute component onError first, then default onError', async () => { @@ -615,7 +618,7 @@ describe('lazy', () => { const mockImport = importCache.createImport({ failureCount: 10, failureDelay: 100, successDelay: 50 }) const Component = lazy(() => mockImport('/test-component')) - // Get the load function and set storage to the limit + // Set storage to the limit using the original load's toString (which is the storage key) const loadFunction = Component.load storage.setItem(loadFunction.toString(), '1') @@ -717,7 +720,10 @@ describe('lazy', () => { // Component's onError should also be called expect(individualOnError).toHaveBeenCalledTimes(1) - expect(individualOnError).toHaveBeenCalledWith({ error: expect.any(Error), load: expect.any(Function) }) + expect(individualOnError).toHaveBeenCalledWith({ + error: expect.any(Error), + [reloadOnErrorStorageKeyAccessKeySymbol]: expect.any(String), + }) await act(() => vi.advanceTimersByTimeAsync(1)) expect(mockReload).toHaveBeenCalledTimes(1) }) @@ -749,7 +755,10 @@ describe('lazy', () => { // Component's onError should also be called expect(individualOnError).toHaveBeenCalledTimes(1) - expect(individualOnError).toHaveBeenCalledWith({ error: expect.any(Error), load: expect.any(Function) }) + expect(individualOnError).toHaveBeenCalledWith({ + error: expect.any(Error), + [reloadOnErrorStorageKeyAccessKeySymbol]: expect.any(String), + }) await act(() => vi.advanceTimersByTimeAsync(1)) // reloadOnError should work expect(mockReload).toHaveBeenCalledTimes(1) @@ -788,9 +797,15 @@ describe('lazy', () => { // Factory's onError should also be called expect(defaultOnError).toHaveBeenCalledTimes(1) - expect(defaultOnError).toHaveBeenCalledWith({ error: expect.any(Error), load: expect.any(Function) }) + expect(defaultOnError).toHaveBeenCalledWith({ + error: expect.any(Error), + [reloadOnErrorStorageKeyAccessKeySymbol]: expect.any(String), + }) expect(individualOnError).toHaveBeenCalledTimes(1) - expect(individualOnError).toHaveBeenCalledWith({ error: expect.any(Error), load: expect.any(Function) }) + expect(individualOnError).toHaveBeenCalledWith({ + error: expect.any(Error), + [reloadOnErrorStorageKeyAccessKeySymbol]: expect.any(String), + }) expect(defaultOnSuccess).toHaveBeenCalledTimes(0) expect(individualOnSuccess).toHaveBeenCalledTimes(0) await act(() => vi.advanceTimersByTimeAsync(1)) @@ -815,8 +830,10 @@ describe('lazy', () => { expect(defaultOnError).toHaveBeenCalledTimes(1) expect(individualOnError).toHaveBeenCalledTimes(1) - expect(defaultOnSuccess).toHaveBeenCalledWith({ load: expect.any(Function) }) - expect(individualOnSuccess).toHaveBeenCalledWith({ load: expect.any(Function) }) + expect(defaultOnSuccess).toHaveBeenCalledWith({ [reloadOnErrorStorageKeyAccessKeySymbol]: expect.any(String) }) + expect(individualOnSuccess).toHaveBeenCalledWith({ + [reloadOnErrorStorageKeyAccessKeySymbol]: expect.any(String), + }) expect(mockReload).toHaveBeenCalledTimes(1) }) }) diff --git a/packages/react/src/lazy.ts b/packages/react/src/lazy.ts index 7a3f8da0e..7e537827e 100644 --- a/packages/react/src/lazy.ts +++ b/packages/react/src/lazy.ts @@ -1,9 +1,11 @@ 'use client' import { type ComponentType, type LazyExoticComponent, lazy as originalLazy } from 'react' +export const reloadOnErrorStorageKeyAccessKeySymbol = Symbol('reloadOnErrorStorageKeyAccessKey') + interface LazyOptions { - onSuccess?: ({ load }: { load: () => Promise<{ default: ComponentType }> }) => void - onError?: ({ error, load }: { error: unknown; load: () => Promise<{ default: ComponentType }> }) => void + onSuccess?: () => void + onError?: (options: { error: unknown }) => void } /** @@ -47,14 +49,31 @@ export const createLazy = ): LazyExoticComponent & { load: () => Promise<{ default: T }> } => { + const storageKey = load.toString() + const composedOnSuccess = () => { - options?.onSuccess?.({ load }) - defaultOptions.onSuccess?.({ load }) + ;(options?.onSuccess as undefined | ((arg: { [reloadOnErrorStorageKeyAccessKeySymbol]: string }) => void))?.({ + [reloadOnErrorStorageKeyAccessKeySymbol]: storageKey, + }) + ;( + defaultOptions.onSuccess as undefined | ((arg: { [reloadOnErrorStorageKeyAccessKeySymbol]: string }) => void) + )?.({ [reloadOnErrorStorageKeyAccessKeySymbol]: storageKey }) } const composedOnError = (error: unknown) => { - options?.onError?.({ error, load }) - defaultOptions.onError?.({ error, load }) + ;( + options?.onError as + | undefined + | ((arg: { error: unknown; [reloadOnErrorStorageKeyAccessKeySymbol]: string }) => void) + )?.({ + error, + [reloadOnErrorStorageKeyAccessKeySymbol]: storageKey, + }) + ;( + defaultOptions.onError as + | undefined + | ((arg: { error: unknown; [reloadOnErrorStorageKeyAccessKeySymbol]: string }) => void) + )?.({ error, [reloadOnErrorStorageKeyAccessKeySymbol]: storageKey }) } return Object.assign( @@ -188,14 +207,14 @@ export const reloadOnError = ({ return { ...options, - onSuccess: ({ load }) => { - options.onSuccess?.({ load }) - reloadStorage.removeItem(load.toString()) - }, - onError: ({ error, load }) => { - options.onError?.({ error, load }) - - const storageKey = load.toString() + onSuccess: ((arg: any) => { + ;(options.onSuccess as any)?.(arg) + const storageKey = arg?.[reloadOnErrorStorageKeyAccessKeySymbol] as string + reloadStorage.removeItem(storageKey) + }) as LazyOptions['onSuccess'], + onError: (errorOptions: { error: unknown }) => { + options.onError?.(errorOptions) + const storageKey = (errorOptions as any)[reloadOnErrorStorageKeyAccessKeySymbol] as string let currentRetryCount = 0 if (typeof retry === 'number') { From ac2898982916e866918f6de3a20ec8e8284c328c Mon Sep 17 00:00:00 2001 From: Jonghyeon Ko Date: Fri, 3 Apr 2026 20:13:39 +0900 Subject: [PATCH 2/3] fix(react): Rename reloadOnErrorStorageKeyAccessKeySymbol to reloadOnErrorStorageKeySymbol --- packages/react/src/lazy.spec.tsx | 16 +++++++-------- packages/react/src/lazy.ts | 34 +++++++++++++++----------------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/packages/react/src/lazy.spec.tsx b/packages/react/src/lazy.spec.tsx index f1ead2d35..6436f540a 100644 --- a/packages/react/src/lazy.spec.tsx +++ b/packages/react/src/lazy.spec.tsx @@ -3,7 +3,7 @@ import { act, render, screen } from '@testing-library/react' import { type ComponentType, Suspense, useState } from 'react' import { afterEach, describe, expect, it, vi } from 'vitest' import { ErrorBoundary } from './ErrorBoundary' -import { createLazy, lazy, reloadOnError, reloadOnErrorStorageKeyAccessKeySymbol } from './lazy' +import { createLazy, lazy, reloadOnError, reloadOnErrorStorageKeySymbol } from './lazy' import { sleep } from './test-utils' type PathData = { @@ -319,7 +319,7 @@ describe('lazy', () => { expect(onError).toHaveBeenCalledTimes(1) expect(onError).toHaveBeenCalledWith({ error: expect.any(Error), - [reloadOnErrorStorageKeyAccessKeySymbol]: expect.any(String), + [reloadOnErrorStorageKeySymbol]: expect.any(String), }) }) @@ -722,7 +722,7 @@ describe('lazy', () => { expect(individualOnError).toHaveBeenCalledTimes(1) expect(individualOnError).toHaveBeenCalledWith({ error: expect.any(Error), - [reloadOnErrorStorageKeyAccessKeySymbol]: expect.any(String), + [reloadOnErrorStorageKeySymbol]: expect.any(String), }) await act(() => vi.advanceTimersByTimeAsync(1)) expect(mockReload).toHaveBeenCalledTimes(1) @@ -757,7 +757,7 @@ describe('lazy', () => { expect(individualOnError).toHaveBeenCalledTimes(1) expect(individualOnError).toHaveBeenCalledWith({ error: expect.any(Error), - [reloadOnErrorStorageKeyAccessKeySymbol]: expect.any(String), + [reloadOnErrorStorageKeySymbol]: expect.any(String), }) await act(() => vi.advanceTimersByTimeAsync(1)) // reloadOnError should work @@ -799,12 +799,12 @@ describe('lazy', () => { expect(defaultOnError).toHaveBeenCalledTimes(1) expect(defaultOnError).toHaveBeenCalledWith({ error: expect.any(Error), - [reloadOnErrorStorageKeyAccessKeySymbol]: expect.any(String), + [reloadOnErrorStorageKeySymbol]: expect.any(String), }) expect(individualOnError).toHaveBeenCalledTimes(1) expect(individualOnError).toHaveBeenCalledWith({ error: expect.any(Error), - [reloadOnErrorStorageKeyAccessKeySymbol]: expect.any(String), + [reloadOnErrorStorageKeySymbol]: expect.any(String), }) expect(defaultOnSuccess).toHaveBeenCalledTimes(0) expect(individualOnSuccess).toHaveBeenCalledTimes(0) @@ -830,9 +830,9 @@ describe('lazy', () => { expect(defaultOnError).toHaveBeenCalledTimes(1) expect(individualOnError).toHaveBeenCalledTimes(1) - expect(defaultOnSuccess).toHaveBeenCalledWith({ [reloadOnErrorStorageKeyAccessKeySymbol]: expect.any(String) }) + expect(defaultOnSuccess).toHaveBeenCalledWith({ [reloadOnErrorStorageKeySymbol]: expect.any(String) }) expect(individualOnSuccess).toHaveBeenCalledWith({ - [reloadOnErrorStorageKeyAccessKeySymbol]: expect.any(String), + [reloadOnErrorStorageKeySymbol]: expect.any(String), }) expect(mockReload).toHaveBeenCalledTimes(1) }) diff --git a/packages/react/src/lazy.ts b/packages/react/src/lazy.ts index 7e537827e..4ddf769e0 100644 --- a/packages/react/src/lazy.ts +++ b/packages/react/src/lazy.ts @@ -1,7 +1,7 @@ 'use client' import { type ComponentType, type LazyExoticComponent, lazy as originalLazy } from 'react' -export const reloadOnErrorStorageKeyAccessKeySymbol = Symbol('reloadOnErrorStorageKeyAccessKey') +export const reloadOnErrorStorageKeySymbol = Symbol('reloadOnErrorStorageKey') interface LazyOptions { onSuccess?: () => void @@ -52,28 +52,26 @@ export const createLazy = const storageKey = load.toString() const composedOnSuccess = () => { - ;(options?.onSuccess as undefined | ((arg: { [reloadOnErrorStorageKeyAccessKeySymbol]: string }) => void))?.({ - [reloadOnErrorStorageKeyAccessKeySymbol]: storageKey, + ;(options?.onSuccess as undefined | ((arg: { [reloadOnErrorStorageKeySymbol]: string }) => void))?.({ + [reloadOnErrorStorageKeySymbol]: storageKey, + }) + ;(defaultOptions.onSuccess as undefined | ((arg: { [reloadOnErrorStorageKeySymbol]: string }) => void))?.({ + [reloadOnErrorStorageKeySymbol]: storageKey, }) - ;( - defaultOptions.onSuccess as undefined | ((arg: { [reloadOnErrorStorageKeyAccessKeySymbol]: string }) => void) - )?.({ [reloadOnErrorStorageKeyAccessKeySymbol]: storageKey }) } const composedOnError = (error: unknown) => { - ;( - options?.onError as - | undefined - | ((arg: { error: unknown; [reloadOnErrorStorageKeyAccessKeySymbol]: string }) => void) - )?.({ - error, - [reloadOnErrorStorageKeyAccessKeySymbol]: storageKey, - }) + ;(options?.onError as undefined | ((arg: { error: unknown; [reloadOnErrorStorageKeySymbol]: string }) => void))?.( + { + error, + [reloadOnErrorStorageKeySymbol]: storageKey, + } + ) ;( defaultOptions.onError as | undefined - | ((arg: { error: unknown; [reloadOnErrorStorageKeyAccessKeySymbol]: string }) => void) - )?.({ error, [reloadOnErrorStorageKeyAccessKeySymbol]: storageKey }) + | ((arg: { error: unknown; [reloadOnErrorStorageKeySymbol]: string }) => void) + )?.({ error, [reloadOnErrorStorageKeySymbol]: storageKey }) } return Object.assign( @@ -209,12 +207,12 @@ export const reloadOnError = ({ ...options, onSuccess: ((arg: any) => { ;(options.onSuccess as any)?.(arg) - const storageKey = arg?.[reloadOnErrorStorageKeyAccessKeySymbol] as string + const storageKey = arg?.[reloadOnErrorStorageKeySymbol] as string reloadStorage.removeItem(storageKey) }) as LazyOptions['onSuccess'], onError: (errorOptions: { error: unknown }) => { options.onError?.(errorOptions) - const storageKey = (errorOptions as any)[reloadOnErrorStorageKeyAccessKeySymbol] as string + const storageKey = (errorOptions as any)[reloadOnErrorStorageKeySymbol] as string let currentRetryCount = 0 if (typeof retry === 'number') { From 73fcb86cba0c1bdf9449e42918eb6373f8fb5cf6 Mon Sep 17 00:00:00 2001 From: Jonghyeon Ko Date: Fri, 3 Apr 2026 20:26:56 +0900 Subject: [PATCH 3/3] refactor(react): Remove reloadOnErrorStorageKeySymbol from onError expectations in tests --- packages/react/src/lazy.spec.tsx | 33 ++++++++------------------------ packages/react/src/lazy.ts | 2 +- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/packages/react/src/lazy.spec.tsx b/packages/react/src/lazy.spec.tsx index 6436f540a..3d9cbccba 100644 --- a/packages/react/src/lazy.spec.tsx +++ b/packages/react/src/lazy.spec.tsx @@ -3,7 +3,7 @@ import { act, render, screen } from '@testing-library/react' import { type ComponentType, Suspense, useState } from 'react' import { afterEach, describe, expect, it, vi } from 'vitest' import { ErrorBoundary } from './ErrorBoundary' -import { createLazy, lazy, reloadOnError, reloadOnErrorStorageKeySymbol } from './lazy' +import { createLazy, lazy, reloadOnError } from './lazy' import { sleep } from './test-utils' type PathData = { @@ -317,10 +317,7 @@ describe('lazy', () => { expect(screen.getByText('error')).toBeInTheDocument() expect(onError).toHaveBeenCalledTimes(1) - expect(onError).toHaveBeenCalledWith({ - error: expect.any(Error), - [reloadOnErrorStorageKeySymbol]: expect.any(String), - }) + expect(onError).toHaveBeenCalledWith(expect.objectContaining({ error: expect.any(Error) })) }) it('should execute component onError first, then default onError', async () => { @@ -720,10 +717,7 @@ describe('lazy', () => { // Component's onError should also be called expect(individualOnError).toHaveBeenCalledTimes(1) - expect(individualOnError).toHaveBeenCalledWith({ - error: expect.any(Error), - [reloadOnErrorStorageKeySymbol]: expect.any(String), - }) + expect(individualOnError).toHaveBeenCalledWith(expect.objectContaining({ error: expect.any(Error) })) await act(() => vi.advanceTimersByTimeAsync(1)) expect(mockReload).toHaveBeenCalledTimes(1) }) @@ -755,10 +749,7 @@ describe('lazy', () => { // Component's onError should also be called expect(individualOnError).toHaveBeenCalledTimes(1) - expect(individualOnError).toHaveBeenCalledWith({ - error: expect.any(Error), - [reloadOnErrorStorageKeySymbol]: expect.any(String), - }) + expect(individualOnError).toHaveBeenCalledWith(expect.objectContaining({ error: expect.any(Error) })) await act(() => vi.advanceTimersByTimeAsync(1)) // reloadOnError should work expect(mockReload).toHaveBeenCalledTimes(1) @@ -797,15 +788,9 @@ describe('lazy', () => { // Factory's onError should also be called expect(defaultOnError).toHaveBeenCalledTimes(1) - expect(defaultOnError).toHaveBeenCalledWith({ - error: expect.any(Error), - [reloadOnErrorStorageKeySymbol]: expect.any(String), - }) + expect(defaultOnError).toHaveBeenCalledWith(expect.objectContaining({ error: expect.any(Error) })) expect(individualOnError).toHaveBeenCalledTimes(1) - expect(individualOnError).toHaveBeenCalledWith({ - error: expect.any(Error), - [reloadOnErrorStorageKeySymbol]: expect.any(String), - }) + expect(individualOnError).toHaveBeenCalledWith(expect.objectContaining({ error: expect.any(Error) })) expect(defaultOnSuccess).toHaveBeenCalledTimes(0) expect(individualOnSuccess).toHaveBeenCalledTimes(0) await act(() => vi.advanceTimersByTimeAsync(1)) @@ -830,10 +815,8 @@ describe('lazy', () => { expect(defaultOnError).toHaveBeenCalledTimes(1) expect(individualOnError).toHaveBeenCalledTimes(1) - expect(defaultOnSuccess).toHaveBeenCalledWith({ [reloadOnErrorStorageKeySymbol]: expect.any(String) }) - expect(individualOnSuccess).toHaveBeenCalledWith({ - [reloadOnErrorStorageKeySymbol]: expect.any(String), - }) + expect(defaultOnSuccess).toHaveBeenCalledTimes(1) + expect(individualOnSuccess).toHaveBeenCalledTimes(1) expect(mockReload).toHaveBeenCalledTimes(1) }) }) diff --git a/packages/react/src/lazy.ts b/packages/react/src/lazy.ts index 4ddf769e0..3d5330286 100644 --- a/packages/react/src/lazy.ts +++ b/packages/react/src/lazy.ts @@ -1,7 +1,7 @@ 'use client' import { type ComponentType, type LazyExoticComponent, lazy as originalLazy } from 'react' -export const reloadOnErrorStorageKeySymbol = Symbol('reloadOnErrorStorageKey') +const reloadOnErrorStorageKeySymbol = Symbol('reloadOnErrorStorageKey') interface LazyOptions { onSuccess?: () => void