Skip to content

Commit

Permalink
useExecutorSuspense accepts a single executor
Browse files Browse the repository at this point in the history
  • Loading branch information
smikhalevski committed Dec 2, 2024
1 parent 4db0f3b commit f9be911
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 105 deletions.
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1357,10 +1357,10 @@ import { useExecutor, useExecutorSuspense } from 'react-executor';

const Account = () => {
const accountExecutor = useExecutor('account', signal => {
// Fetch the account from the server
// Fetch an account from a server
});

// Suspend rendering
// Suspend rendering if accountExecutor is pending and isn't fulfilled
useExecutorSuspense(accountExecutor);

// accountExecutor is settled during render
Expand All @@ -1380,13 +1380,29 @@ const App = () => (
);
```

You can provide multiple executors to `useExecutorSuspense` to wait for them in parallel:
## Suspending on external executors

You can use executors created outside the rendering process in your components, rerender and suspend your components
when such executors get updated:

```ts
const accountExecutor = useExecutor('account');
const shoppingCartExecutor = useExecutor('shoppingCart');
const manager = new ExecutorManager();

// 1️⃣ Create an executor
const accountExecutor = useExecutor('account', signal => {
// Fetch an account from a server
});

useExecutorSuspense([accountExecutor, shoppingCartExecutor]);
function Account() {
// 2️⃣ Re-render a component when accountExecutor is updated
useExecutorSubscription(accountExecutor);

// 3️⃣ Suspend rendering if accountExecutor is pending and isn't fulfilled
useExecutorSuspense(accountExecutor);

// 4️⃣ Use a value stored in an executor
const account = accountExecutor.get();
}
```

# Server-side rendering
Expand Down
37 changes: 20 additions & 17 deletions src/main/ExecutorSuspense.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,45 @@
import React, { ReactNode, Suspense } from 'react';
import React, { ReactElement, ReactNode, Suspense } from 'react';
import { Executor } from './types';
import { useExecutorSubscription } from './useExecutorSubscription';
import { useExecutorSuspense } from './useExecutorSuspense';

/**
* Props of {@link ExecutorSuspense}.
*
* @template T Executors to wait for.
* @template Value The value stored by the executor.
*/
export interface ExecutorSuspenseProps<T extends Executor | Executor[]> {
export interface ExecutorSuspenseProps<Value> {
/**
* Executors to wait for.
*/
executors: T;
executor: Executor<Value>;

/**
* Renders contents of {@link executors}.
* Renders contents of {@link executor}.
*/
children: ((executors: T) => ReactNode) | ReactNode;
children: ((executor: Executor<Value>) => ReactNode) | ReactNode;

/**
* The fallback that is rendered when executors are pending.
* The fallback that is rendered when executor are pending.
*/
fallback?: ReactNode;

/**
* The predicate which a pending executor must conform to suspend the rendering process. By default,
* only non-fulfilled executors are awaited.
* only non-fulfilled executor are awaited.
*/
predicate?: (executor: Executor) => boolean;
predicate?: (executor: Executor<Value>) => boolean;
}

/**
* Renders a {@link ExecutorSuspenseProps.fallback fallback} if any of provided
* {@link ExecutorSuspenseProps.executors executors} aren't settled.
* Renders a {@link ExecutorSuspenseProps.fallback fallback} if a provided
* {@link ExecutorSuspenseProps.executor executor} isn't settled.
*
* @template T Executors to wait for.
* @template Value The value stored by the executor.
*/
export function ExecutorSuspense<T extends Executor | Executor[]>(props: ExecutorSuspenseProps<T>) {
export function ExecutorSuspense<Value>(props: ExecutorSuspenseProps<Value>): ReactElement {
useExecutorSubscription(props.executor);

return (
<Suspense fallback={props.fallback}>
<ExecutorSuspenseContent {...props} />
Expand All @@ -46,12 +49,12 @@ export function ExecutorSuspense<T extends Executor | Executor[]>(props: Executo

ExecutorSuspense.displayName = 'ExecutorSuspense';

function ExecutorSuspenseContent(props: ExecutorSuspenseProps<any>): ReactNode {
const { children, executors } = props;
function ExecutorSuspenseContent<Value>(props: ExecutorSuspenseProps<Value>): ReactNode {
const { children, executor } = props;

useExecutorSuspense(executors, props.predicate);
useExecutorSuspense(executor, props.predicate);

return typeof children === 'function' ? children(executors) : children;
return typeof children === 'function' ? children(executor) : children;
}

ExecutorSuspenseContent.displayName = 'ExecutorSuspenseContent';
36 changes: 13 additions & 23 deletions src/main/useExecutorSuspense.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,22 @@
import type { Executor } from './types';
import { noop } from './utils';

/**
* Suspends rendering until all of provided executors are settled.
* Suspends rendering until an executor satisfies a predicate.
*
* @param executors Executors to wait for.
* @param executor An executor to wait for.
* @param predicate The predicate which a pending executor must conform to suspend the rendering process. By default,
* only non-fulfilled executors are awaited.
* @template T Executors to wait for.
* executor is awaited if it isn't fulfilled.
* @template Value The value stored by the executor.
*/
export function useExecutorSuspense<T extends Executor | Executor[]>(
executors: T,
predicate = (executor: Executor) => !executor.isFulfilled
): T {
if (Array.isArray(executors)) {
const promises = [];

for (const executor of executors) {
if (executor.isPending && predicate(executor)) {
promises.push(executor.getOrAwait().then(noop, noop));
}
}
if (promises.length !== 0) {
throw Promise.all(promises);
}
} else if (executors.isPending && predicate(executors)) {
throw executors.getOrAwait().then(noop, noop);
export function useExecutorSuspense<Value>(
executor: Executor<Value>,
predicate: (executor: Executor<Value>) => boolean = isUnfulfilled
): void {
if (executor.isPending && predicate(executor)) {
throw executor.getOrAwait();
}
}

return executors;
function isUnfulfilled(executor: Executor) {
return !executor.isFulfilled;
}
34 changes: 3 additions & 31 deletions src/test/ExecutorSuspense.test.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,9 @@
import '@testing-library/jest-dom';
import { render } from '@testing-library/react';
import React, { useEffect } from 'react';
import {
ExecutorManager,
ExecutorManagerProvider,
ExecutorSuspense,
useExecutor,
useExecutorSubscription,
} from '../main';
import { ExecutorManager, ExecutorManagerProvider, ExecutorSuspense, useExecutorSubscription } from '../main';

describe('ExecutorSuspense', () => {
test('suspends component rendering until executors are settled', async () => {
const Component = () => {
const executor1 = useExecutor('xxx', () => 'aaa');
const executor2 = useExecutor('yyy', () => 'bbb');

return (
<ExecutorSuspense
fallback={'ccc'}
executors={[executor1, executor2]}
>
{executors => executors[0].get() + executors[1].get()}
</ExecutorSuspense>
);
};

const result = render(<Component />);

expect(result.getByText('ccc')).toBeInTheDocument();

expect(await result.findByText('aaabbb')).toBeInTheDocument();
});

test('does not suspend rendering if the pending executor is settled', async () => {
const manager = new ExecutorManager();
const capture = jest.fn();
Expand All @@ -46,7 +18,7 @@ describe('ExecutorSuspense', () => {
}, []);

return (
<ExecutorSuspense executors={executor}>
<ExecutorSuspense executor={executor}>
{executor => {
capture(executor.value);
return executor.value;
Expand Down Expand Up @@ -85,7 +57,7 @@ describe('ExecutorSuspense', () => {

return (
<ExecutorSuspense
executors={executor}
executor={executor}
predicate={predicateMock}
>
{executor => {
Expand Down
29 changes: 1 addition & 28 deletions src/test/useExecutorSuspense.test.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,9 @@
import '@testing-library/jest-dom';
import { render } from '@testing-library/react';
import React, { Suspense, useEffect } from 'react';
import {
ExecutorManager,
ExecutorManagerProvider,
useExecutor,
useExecutorSubscription,
useExecutorSuspense,
} from '../main';
import { ExecutorManager, ExecutorManagerProvider, useExecutorSubscription, useExecutorSuspense } from '../main';

describe('useExecutorSuspense', () => {
test('suspends component rendering until executors are settled', async () => {
const Component = () => {
const executor1 = useExecutor('xxx', () => 'aaa');
const executor2 = useExecutor('yyy', () => 'bbb');

useExecutorSuspense([executor1, executor2]);

return executor1.get() + executor2.get();
};

const result = render(
<Suspense fallback={'ccc'}>
<Component />
</Suspense>
);

expect(result.getByText('ccc')).toBeInTheDocument();

expect(await result.findByText('aaabbb')).toBeInTheDocument();
});

test('does not suspend rendering if the pending executor is settled', async () => {
const manager = new ExecutorManager();
const capture = jest.fn();
Expand Down

0 comments on commit f9be911

Please sign in to comment.