Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions src/lifecycle/components/TransitionPresence/TransitionPresence.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,24 +71,30 @@ export function TransitionPresence({
const abortController = new AbortController();

(async (): Promise<void> => {
onPreviousChildrenUnmountingRef.current?.(previousChildren, children);
try {
onPreviousChildrenUnmountingRef.current?.(previousChildren, children);

// Defer children update for before unmount lifecycle
await beforeUnmountPreviousChildren(abortController.signal);
// Defer children update for before unmount lifecycle
await beforeUnmountPreviousChildren(abortController.signal);

setPreviousChildren(null);
setPreviousChildren(null);

// Wait a tick after removing previous children to make sure new children
// are re-initialized
await createTimeout();
// Wait a tick after removing previous children to make sure new children
// are re-initialized
await createTimeout(0, { signal: abortController.signal });

onPreviousChildrenUnmountedRef.current?.(previousChildren, children);
onPreviousChildrenUnmountedRef.current?.(previousChildren, children);

// Set new children
setPreviousChildren(children);
await createTimeout();
// Set new children
setPreviousChildren(children);
await createTimeout(0, { signal: abortController.signal });

onChildrenMountedRef.current?.(previousChildren, children);
onChildrenMountedRef.current?.(previousChildren, children);
} catch (error) {
if (error !== abortController.signal.reason) {
throw new Error(`Unexpected error in TransitionPresence transition`, { cause: error });
}
}
Comment thread
ThaNarie marked this conversation as resolved.
})();

return () => {
Expand Down
28 changes: 26 additions & 2 deletions src/utils/createTimeout/createTimeout.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ Creates a promise that resolves after given timeout.
## Reference

```ts
createTimeout(ms = 0): Promise<void>
createTimeout(ms = 0, options?: { signal?: AbortSignal }): Promise<void>
```

### Parameters

- `ms` - The number of milliseconds to wait before resolving. Defaults to `0`.
- `options.signal` - An optional `AbortSignal` to cancel the timeout. When aborted, the promise
rejects with the signal's reason.

### Returns

Promise that resolves after given timeout.
Promise that resolves after given timeout, or rejects if the provided `signal` is aborted.

## Usage

Expand All @@ -26,6 +32,24 @@ await createTimeout(1000);
console.log("I'm called after 1 second");
```

Using with an `AbortSignal` to cancel the timeout

```ts
const controller = new AbortController();

// Abort the timeout after 500ms
setTimeout(() => {
controller.abort();
}, 500);

try {
await createTimeout(2000, { signal: controller.signal });
console.log('Timeout completed');
} catch {
console.log('Timeout was cancelled');
}
```

Using `Promise.then`

```ts
Expand Down
31 changes: 31 additions & 0 deletions src/utils/createTimeout/createTimeout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,35 @@ describe('createTimeout', () => {

expect(currentTime - startTime >= timeout).toBe(true);
});

it('should reject immediately when signal is already aborted', async () => {
const controller = new AbortController();
controller.abort();

await expect(createTimeout(100, { signal: controller.signal })).rejects.toThrow();
});

it('should reject when signal is aborted before timeout', async () => {
const controller = new AbortController();

const promise = createTimeout(1000, { signal: controller.signal });

controller.abort();

await expect(promise).rejects.toThrow();
});

it('should resolve normally when no signal is provided', async () => {
await expect(createTimeout(0)).resolves.toBeUndefined();
});

it('should reject with the abort reason', async () => {
const controller = new AbortController();
const reason = new Error('custom abort reason');
controller.abort(reason);

await expect(createTimeout(100, { signal: controller.signal })).rejects.toThrow(
'custom abort reason',
);
});
});
30 changes: 27 additions & 3 deletions src/utils/createTimeout/createTimeout.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
/**
* Returns a promise that resolves after given amount of time. Use the default
* value of `0` to wait for the next tick.
*
* @param ms - The number of milliseconds to wait before resolving. Defaults to `0`.
* @param options - Optional options object.
* @param options.signal - An optional AbortSignal to cancel the timeout. When aborted, the promise rejects with the signal's reason.
*/
export function createTimeout(ms = 0): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
export function createTimeout(ms = 0, { signal }: { signal?: AbortSignal } = {}): Promise<void> {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(signal.reason);
return;
}

if (signal === undefined) {
setTimeout(resolve, ms);
return;
}

const onAbort = (): void => {
clearTimeout(timeoutId);
reject(signal.reason);
};

const timeoutId = setTimeout(() => {
signal.removeEventListener('abort', onAbort);
resolve();
}, ms);

signal.addEventListener('abort', onAbort, { once: true });
});
}
Loading