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
2 changes: 1 addition & 1 deletion types/react-is/test/next-tests.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// <reference types="../../react/next"/>
/// <reference types="../../react/experimental"/>
import * as React from 'react';
import * as ReactIs from 'react-is';
import 'react-is/next';
Expand Down
60 changes: 51 additions & 9 deletions types/react/experimental.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,60 @@ import React = require('./next');
export {};

declare module '.' {
export type SuspenseListRevealOrder = 'forwards' | 'backwards' | 'together';
export type SuspenseListTailMode = 'collapsed' | 'hidden';

export interface SuspenseListCommonProps {
/**
* Note that SuspenseList require more than one child;
* it is a runtime warning to provide only a single child.
*
* It does, however, allow those children to be wrapped inside a single
* level of `<React.Fragment>`.
*/
children: ReactElement | Iterable<ReactElement>;
}

interface DirectionalSuspenseListProps extends SuspenseListCommonProps {
/**
* Defines the order in which the `SuspenseList` children should be revealed.
*/
revealOrder: 'forwards' | 'backwards';
/**
* Dictates how unloaded items in a SuspenseList is shown.
*
* - By default, `SuspenseList` will show all fallbacks in the list.
* - `collapsed` shows only the next fallback in the list.
* - `hidden` doesn’t show any unloaded items.
*/
tail?: SuspenseListTailMode | undefined;
}

interface NonDirectionalSuspenseListProps extends SuspenseListCommonProps {
/**
* Defines the order in which the `SuspenseList` children should be revealed.
*/
revealOrder?: Exclude<SuspenseListRevealOrder, DirectionalSuspenseListProps['revealOrder']> | undefined;
/**
* The tail property is invalid when not using the `forwards` or `backwards` reveal orders.
*/
tail?: never | undefined;
}

export type SuspenseListProps = DirectionalSuspenseListProps | NonDirectionalSuspenseListProps;

/**
* @param subscribe
* @param getSnapshot
* `SuspenseList` helps coordinate many components that can suspend by orchestrating the order
* in which these components are revealed to the user.
*
* When multiple components need to fetch data, this data may arrive in an unpredictable order.
* However, if you wrap these items in a `SuspenseList`, React will not show an item in the list
* until previous items have been displayed (this behavior is adjustable).
*
* @see https://github.com/reactwg/react-18/discussions/86
* @see https://reactjs.org/docs/concurrent-mode-reference.html#suspenselist
* @see https://reactjs.org/docs/concurrent-mode-patterns.html#suspenselist
*/
// keep in sync with `useSyncExternalStore` from `use-sync-external-store`
export function unstable_useSyncExternalStore<Snapshot>(
subscribe: (onStoreChange: () => void) => () => void,
getSnapshot: () => Snapshot,
getServerSnapshot?: () => Snapshot,
): Snapshot;
export const SuspenseList: ExoticComponent<SuspenseListProps>;

/**
* @param effect Imperative function that can return a cleanup function
Expand Down
68 changes: 13 additions & 55 deletions types/react/next.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,61 +42,6 @@ declare module '.' {
unstable_expectedLoadTime?: number | undefined;
}

export type SuspenseListRevealOrder = 'forwards' | 'backwards' | 'together';
export type SuspenseListTailMode = 'collapsed' | 'hidden';

export interface SuspenseListCommonProps {
/**
* Note that SuspenseList require more than one child;
* it is a runtime warning to provide only a single child.
*
* It does, however, allow those children to be wrapped inside a single
* level of `<React.Fragment>`.
*/
children: ReactElement | Iterable<ReactElement>;
}

interface DirectionalSuspenseListProps extends SuspenseListCommonProps {
/**
* Defines the order in which the `SuspenseList` children should be revealed.
*/
revealOrder: 'forwards' | 'backwards';
/**
* Dictates how unloaded items in a SuspenseList is shown.
*
* - By default, `SuspenseList` will show all fallbacks in the list.
* - `collapsed` shows only the next fallback in the list.
* - `hidden` doesn’t show any unloaded items.
*/
tail?: SuspenseListTailMode | undefined;
}

interface NonDirectionalSuspenseListProps extends SuspenseListCommonProps {
/**
* Defines the order in which the `SuspenseList` children should be revealed.
*/
revealOrder?: Exclude<SuspenseListRevealOrder, DirectionalSuspenseListProps['revealOrder']> | undefined;
/**
* The tail property is invalid when not using the `forwards` or `backwards` reveal orders.
*/
tail?: never | undefined;
}

export type SuspenseListProps = DirectionalSuspenseListProps | NonDirectionalSuspenseListProps;

/**
* `SuspenseList` helps coordinate many components that can suspend by orchestrating the order
* in which these components are revealed to the user.
*
* When multiple components need to fetch data, this data may arrive in an unpredictable order.
* However, if you wrap these items in a `SuspenseList`, React will not show an item in the list
* until previous items have been displayed (this behavior is adjustable).
*
* @see https://reactjs.org/docs/concurrent-mode-reference.html#suspenselist
* @see https://reactjs.org/docs/concurrent-mode-patterns.html#suspenselist
*/
export const SuspenseList: ExoticComponent<SuspenseListProps>;

// must be synchronous
export type TransitionFunction = () => VoidOrUndefinedOnly;
// strange definition to allow vscode to show documentation on the invocation
Expand Down Expand Up @@ -196,4 +141,17 @@ declare module '.' {
* @see https://github.com/reactjs/rfcs/blob/master/text/0147-use-mutable-source.md
*/
export function unstable_useMutableSource<T, TResult extends unknown>(MutableSource: MutableSource<T>, getSnapshot: (source: T) => TResult, subscribe: MutableSourceSubscribe<T>): TResult;

/**
* @param subscribe
* @param getSnapshot
*
* @see https://github.com/reactwg/react-18/discussions/86
*/
// keep in sync with `useSyncExternalStore` from `use-sync-external-store`
export function useSyncExternalStore<Snapshot>(
subscribe: (onStoreChange: () => void) => () => void,
getSnapshot: () => Snapshot,
getServerSnapshot?: () => Snapshot,
): Snapshot;
}
61 changes: 21 additions & 40 deletions types/react/test/experimental.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,6 @@

import React = require('react');

const { unstable_useSyncExternalStore: useSyncExternalStore } = React;
// keep in sync with `use-sync-external-store-tests.ts`
interface Store<State> {
getState(): State;
getServerState(): State;
subscribe(onStoreChange: () => void): () => void;
}

declare const numberStore: Store<number>;
function useVersion(): number {
return useSyncExternalStore(numberStore.subscribe, numberStore.getState);
}

function useStoreWrong() {
useSyncExternalStore(
// no unsubscribe returned
// $ExpectError
() => {
return null;
},
() => 1,
);

// `string` is not assignable to `number`
// $ExpectError
const version: number = useSyncExternalStore(
() => () => {},
() => '1',
);
}

declare const objectStore: Store<{ version: { major: number; minor: number }; users: string[] }>;
function useUsers(): string[] {
return useSyncExternalStore(
objectStore.subscribe,
() => objectStore.getState().users,
() => objectStore.getServerState().users,
);
}

function useExperimentalHooks() {
const [toggle, setToggle] = React.useState(false);

Expand All @@ -51,3 +11,24 @@ function useExperimentalHooks() {
return () => {};
}, [toggle]);
}

// Unsupported `revealOrder` triggers a runtime warning
// $ExpectError
<React.SuspenseList revealOrder="something">
<React.Suspense fallback="Loading">Content</React.Suspense>
</React.SuspenseList>;

<React.SuspenseList revealOrder="backwards">
<React.Suspense fallback="Loading">A</React.Suspense>
<React.Suspense fallback="Loading">B</React.Suspense>
</React.SuspenseList>;

<React.SuspenseList revealOrder="forwards">
<React.Suspense fallback="Loading">A</React.Suspense>
<React.Suspense fallback="Loading">B</React.Suspense>
</React.SuspenseList>;

<React.SuspenseList revealOrder="together">
<React.Suspense fallback="Loading">A</React.Suspense>
<React.Suspense fallback="Loading">B</React.Suspense>
</React.SuspenseList>;
40 changes: 40 additions & 0 deletions types/react/test/next.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import React = require('react');

const { useSyncExternalStore } = React;
// We need these Window interfaces to compile
interface Window {
location: {
Expand Down Expand Up @@ -130,3 +131,42 @@ function SuspenseTest() {
// TODO(react18): Remove.
<React.Suspense fallback={undefined!}></React.Suspense>;
}

// keep in sync with `use-sync-external-store-tests.ts`
interface Store<State> {
getState(): State;
getServerState(): State;
subscribe(onStoreChange: () => void): () => void;
}

declare const numberStore: Store<number>;
function useVersion(): number {
return useSyncExternalStore(numberStore.subscribe, numberStore.getState);
}

function useStoreWrong() {
useSyncExternalStore(
// no unsubscribe returned
// $ExpectError
() => {
return null;
},
() => 1,
);

// `string` is not assignable to `number`
// $ExpectError
const version: number = useSyncExternalStore(
() => () => {},
() => '1',
);
}

declare const objectStore: Store<{ version: { major: number; minor: number }; users: string[] }>;
function useUsers(): string[] {
return useSyncExternalStore(
objectStore.subscribe,
() => objectStore.getState().users,
() => objectStore.getServerState().users,
);
}