diff --git a/README.md b/README.md index 919c8a179..3ca1dba77 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,10 @@ React has documentation for [how to start a new React project](https://react.dev - [Gatsby](https://www.gatsbyjs.com/docs/how-to/custom-configuration/typescript/): `npm init gatsby --ts` - [Expo](https://docs.expo.dev/guides/typescript/): `npx create-expo-app -t with-typescript` +If you just want a client-side single-page app without a framework, [Vite](https://vitejs.dev/) is the most common choice: + +- [Vite](https://vitejs.dev/guide/): `npm create vite@latest my-app -- --template react-ts` + #### Try React and TypeScript online There are some tools that let you run React and TypeScript online, which can be helpful for debugging or making sharable reproductions. @@ -406,11 +410,11 @@ function DelayedEffect(props: { timerMs: number }) { #### useRef -In TypeScript, `useRef` returns a reference that is either [read-only](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/abd69803c1b710db58d511f4544ec1b70bc9077c/types/react/v16/index.d.ts#L1025-L1039) or [mutable](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/abd69803c1b710db58d511f4544ec1b70bc9077c/types/react/v16/index.d.ts#L1012-L1023), depends on whether your type argument fully covers the initial value or not. Choose one that suits your use case. +`useRef` always returns a `RefObject` in current `@types/react`. An initial value is required, and the returned `.current` is typed based on it. (`MutableRefObject` is deprecated and only kept for backwards compatibility.) ##### Option 1: DOM element ref -**[To access a DOM element](https://reactjs.org/docs/refs-and-the-dom.html):** provide only the element type as argument, and use `null` as initial value. In this case, the returned reference will have a read-only `.current` that is managed by React. TypeScript expects you to give this ref to an element's `ref` prop: +**To access a DOM element:** provide the element type as a generic and pass `null` as the initial value. React manages `.current` for you, and TypeScript expects you to pass this ref to an element's `ref` prop: ```tsx function Foo() { @@ -454,21 +458,30 @@ Refs demand specificity - it is not enough to just specify any old `HTMLElement` ##### Option 2: Mutable value ref -**[To have a mutable value](https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables):** provide the type you want, and make sure the initial value fully belongs to that type: +**To hold a mutable value across renders without re-rendering on change:** pass the initial value you want — React doesn't manage `.current` for you here, you write to it manually. ```tsx function Foo() { - // Technical-wise, this returns MutableRefObject const intervalRef = useRef(null); - // You manage the ref yourself (that's why it's called MutableRefObject!) useEffect(() => { - intervalRef.current = setInterval(...); - return () => clearInterval(intervalRef.current); + intervalRef.current = window.setInterval(() => { + /* ... */ + }, 1000); + return () => { + if (intervalRef.current !== null) clearInterval(intervalRef.current); + }; }, []); - // The ref is not passed to any element's "ref" prop - return ; + return ( + + ); } ``` @@ -479,19 +492,21 @@ function Foo() { #### useImperativeHandle -Based on this [Stackoverflow answer](https://stackoverflow.com/a/69292925/5415299): +In React 19, `ref` is a regular prop on function components, so `useImperativeHandle` is called with the `ref` prop directly — no `forwardRef` needed. ```tsx // Countdown.tsx +import { useImperativeHandle, Ref } from "react"; -// Define the handle types which will be passed to the forwardRef export type CountdownHandle = { start: () => void; }; -type CountdownProps = {}; +type CountdownProps = { + ref?: Ref; +}; -const Countdown = forwardRef((props, ref) => { +const Countdown = ({ ref }: CountdownProps) => { useImperativeHandle(ref, () => ({ // start() has type inference here start() { @@ -500,12 +515,12 @@ const Countdown = forwardRef((props, ref) => { })); return
Countdown
; -}); +}; ``` ```tsx -// The component uses the Countdown component - +// The component using the Countdown component +import { useEffect, useRef } from "react"; import Countdown, { CountdownHandle } from "./Countdown.tsx"; function App() { @@ -522,9 +537,7 @@ function App() { } ``` -##### See also: - -- [Using ForwardRefRenderFunction](https://stackoverflow.com/a/62258685/5415299) +> If you still maintain code that targets React < 19, see the [forwardRef section](./forward-create-ref.md) for the legacy approach using `forwardRef`. #### Custom Hooks @@ -786,68 +799,38 @@ class Comp extends React.PureComponent { -#### You May Not Need `defaultProps` - -As per [this tweet](https://twitter.com/dan_abramov/status/1133878326358171650), defaultProps will eventually be deprecated. You can check the discussions here: - -- [Original tweet](https://twitter.com/hswolff/status/1133759319571345408) -- More info can also be found in [this article](https://medium.com/@matanbobi/react-defaultprops-is-dying-whos-the-contender-443c19d9e7f1) +#### Function components -The consensus is to use object default values. - -Function Components: +As of React 19, `defaultProps` is **no longer supported on function components**. Use destructuring defaults directly in the parameter list — TypeScript will infer the prop as optional automatically: ```tsx type GreetProps = { age?: number }; -const Greet = ({ age = 21 }: GreetProps) => // etc -``` - -Class Components: - -```tsx -type GreetProps = { - age?: number; +const Greet = ({ age = 21 }: GreetProps) => { + // ... }; - -class Greet extends React.Component { - render() { - const { age = 21 } = this.props; - /*...*/ - } -} - -let el = ; ``` -#### Typing `defaultProps` - -Type inference improved greatly for `defaultProps` in [TypeScript 3.0+](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html), although [some edge cases are still problematic](https://github.com/typescript-cheatsheets/react/issues/61). - -**Function Components** +If you prefer to declare defaults separately (for example, to share them across components), pull them into a constant and spread them when destructuring: ```tsx -// using typeof as a shortcut; note that it hoists! -// you can also declare the type of DefaultProps if you choose -// e.g. https://github.com/typescript-cheatsheets/react/issues/415#issuecomment-841223219 -type GreetProps = { age: number } & typeof defaultProps; +type GreetProps = { age?: number }; -const defaultProps = { - age: 21, -}; +const defaultProps = { age: 21 } satisfies GreetProps; -const Greet = (props: GreetProps) => { - // etc +const Greet = ({ age = defaultProps.age }: GreetProps) => { + // ... }; -Greet.defaultProps = defaultProps; ``` -_[See this in TS Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAKjgQwM5wEoFNkGN4BmUEIcARFDvmQNwBQdMAnmFnAOKVYwAKxY6ALxwA3igDmWAFxwAdgFcQAIyxQ4AXzgAyOM1YQCcACZYCyeQBte-VPVwRZqeCbOXrEAXGEi6cCdLgAJgBGABo6dXo6e0d4TixuLzgACjAbGXjuPg9UAEovAD5RXzhKGHkoWTgAHiNgADcCkTScgDpkSTgAeiQFZVVELvVqrrrGiPpMmFaXcytsz2FZtwXbOiA)_ +> Setting `Greet.defaultProps = { age: 21 }` will not work on function components in React 19 — the value is ignored at runtime and `FunctionComponent` no longer types it. -For **Class components**, there are [a couple ways to do it](https://github.com/typescript-cheatsheets/react/pull/103#issuecomment-481061483) (including using the `Pick` utility type) but the recommendation is to "reverse" the props definition: +#### Class components + +Class components still support `static defaultProps`. The recommended approach is to type the props with the defaulted keys as required, and let `LibraryManagedAttributes` (applied automatically by JSX) make them optional at the call site: ```tsx -type GreetProps = typeof Greet.defaultProps & { +type GreetProps = { age: number; }; @@ -855,27 +838,28 @@ class Greet extends React.Component { static defaultProps = { age: 21, }; - /*...*/ + + render() { + return
Hello, I am {this.props.age}
; + } } -// Type-checks! No type assertions needed! -let el = ; +// Type-checks — `age` is optional at the call site thanks to defaultProps. +const el = ; ```
React.JSX.LibraryManagedAttributes nuance for library authors -The above implementations work fine for App creators, but sometimes you want to be able to export `GreetProps` so that others can consume it. The problem here is that the way `GreetProps` is defined, `age` is a required prop when it isn't because of `defaultProps`. - -The insight to have here is that [`GreetProps` is the _internal_ contract for your component, not the _external_, consumer facing contract](https://github.com/typescript-cheatsheets/react/issues/66#issuecomment-453878710). You could create a separate type specifically for export, or you could make use of the `React.JSX.LibraryManagedAttributes` utility: +If you export `GreetProps` for consumers, `age` will appear required even though `defaultProps` makes it optional at the call site. `GreetProps` is the _internal_ contract — there's a separate _external_ contract that JSX computes via `React.JSX.LibraryManagedAttributes`. You can compute it explicitly: ```tsx -// internal contract, should not be exported out +// internal contract — don't export type GreetProps = { age: number; }; -class Greet extends Component { +class Greet extends React.Component { static defaultProps = { age: 21 }; } @@ -886,109 +870,7 @@ export type ApparentGreetProps = React.JSX.LibraryManagedAttributes< >; ``` -This will work properly, although hovering over`ApparentGreetProps`may be a little intimidating. You can reduce this boilerplate with the`ComponentProps` utility detailed below. - -
- -#### Consuming Props of a Component with defaultProps - -A component with `defaultProps` may seem to have some required props that actually aren't. - -##### Problem Statement - -Here's what you want to do: - -```tsx -interface IProps { - name: string; -} -const defaultProps = { - age: 25, -}; -const GreetComponent = ({ name, age }: IProps & typeof defaultProps) => ( -
{`Hello, my name is ${name}, ${age}`}
-); -GreetComponent.defaultProps = defaultProps; - -const TestComponent = (props: React.ComponentProps) => { - return

; -}; - -// Property 'age' is missing in type '{ name: string; }' but required in type '{ age: number; }' -const el = ; -``` - -##### Solution - -Define a utility that applies `React.JSX.LibraryManagedAttributes`: - -```tsx -type ComponentProps = T extends - | React.ComponentType - | React.Component - ? React.JSX.LibraryManagedAttributes - : never; - -const TestComponent = (props: ComponentProps) => { - return

; -}; - -// No error -const el = ; -``` - -[_See this in TS Playground_](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAKjgQwM5wEoFNkGN4BmUEIcARFDvmQNwBQdMAnmFnAMImQB2W3MABWJhUAHgAqAPjgBeOOLhYAHjD4ATdNjwwAdJ3ARe-cSyyjg3AlihwB0gD6Yqu-Tz4xzl67cl04cAH44ACkAZQANHQAZYAAjKGQoJgBZZG5kAHMsNQBBGBgoOIBXVTFxABofPzgALjheADdrejoLVSgCPDYASSEIETgAb2r0kCw61AKLDPoAXzpcQ0m4NSxOooAbQWF0OWH-TPG4ACYAVnK6WfpF7mWAcUosGFdDd1k4AApB+uQxysO4LM6r0dnAAGRwZisCAEFZrZCbbb9VAASlk0g+1VEamADUkgwABgAJLAbDYQSogJg-MZwYDoAAkg1GWFmlSZh1mBNmogA9Di8XQUfQHlgni8jLpVustn0BnJpQjZTsWrzeXANsh2gwbstxFhJhK3nIPmAdnUjfw5WIoVgYXBReKuK9+JI0TJpPs4JQYEUoNw4KIABYARjgvN8VwYargADkIIooMQoAslvBSe8JAbns7JTSsDIyAQIBAyOHJDQgA) - -#### Misc Discussions and Knowledge - -
-Why does React.FC break defaultProps? - -You can check the discussions here: - -- https://medium.com/@martin_hotell/10-typescript-pro-tips-patterns-with-or-without-react-5799488d6680 -- https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30695 -- https://github.com/typescript-cheatsheets/react/issues/87 - -This is just the current state and may be fixed in future. - -
- -
-TypeScript 2.9 and earlier - -For TypeScript 2.9 and earlier, there's more than one way to do it, but this is the best advice we've yet seen: - -```ts -type Props = Required & { - /* additional props here */ -}; - -export class MyComponent extends React.Component { - static defaultProps = { - foo: "foo", - }; -} -``` - -Our former recommendation used the `Partial type` feature in TypeScript, which means that the current interface will fulfill a partial version on the wrapped interface. In that way we can extend defaultProps without any changes in the types! - -```ts -interface IMyComponentProps { - firstProp?: string; - secondProp: IPerson[]; -} - -export class MyComponent extends React.Component { - public static defaultProps: Partial = { - firstProp: "default", - }; -} -``` - -The problem with this approach is it causes complex issues with the type inference working with `React.JSX.LibraryManagedAttributes`. Basically it causes the compiler to think that when creating a JSX expression with that component, that all of its props are optional. - -[See commentary by @ferdaber here](https://github.com/typescript-cheatsheets/react/issues/57) and [here](https://github.com/typescript-cheatsheets/react/issues/61). +For most apps this isn't needed — only library authors who re-export the props type tend to hit it.
@@ -1370,7 +1252,7 @@ type ThemeContextType = "light" | "dark"; const ThemeContext = createContext("light"); ``` -Wrap the components that need the context with a context provider: +Wrap the components that need the context by rendering the context itself as a provider. In React 19, the context object can be rendered directly — you no longer need ``: ```tsx import { useState } from "react"; @@ -1379,25 +1261,29 @@ const App = () => { const [theme, setTheme] = useState("light"); return ( - + - + ); }; ``` -Call `useContext` to read and subscribe to the context. +> `` still works and is identical in behavior — it's just the legacy spelling. + +Read the context with `use`: ```tsx -import { useContext } from "react"; +import { use } from "react"; const MyComponent = () => { - const theme = useContext(ThemeContext); + const theme = use(ThemeContext); return

The current theme is {theme}.

; }; ``` +> `useContext(ThemeContext)` still works too. The main difference is that `use` can also unwrap a promise, and it can be called inside conditions and loops. + #### Without default context value If you don't have any meaningful default value, specify `null`: @@ -1419,9 +1305,9 @@ const App = () => { }); return ( - + - + ); }; ``` @@ -1429,10 +1315,10 @@ const App = () => { Now that the type of the context can be `null`, you'll notice that you'll get a `'currentUser' is possibly 'null'` TypeScript error if you try to access the `username` property. You can use optional chaining to access `username`: ```tsx -import { useContext } from "react"; +import { use } from "react"; const MyComponent = () => { - const currentUser = useContext(CurrentUserContext); + const currentUser = use(CurrentUserContext); return

Name: {currentUser?.username}.

; }; @@ -1441,7 +1327,7 @@ const MyComponent = () => { However, it would be preferable to not have to check for `null`, since we know that the context won't be `null`. One way to do that is to provide a custom hook to use the context, where an error is thrown if the context is not provided: ```tsx -import { createContext } from "react"; +import { createContext, use } from "react"; interface CurrentUserContextType { username: string; @@ -1450,11 +1336,11 @@ interface CurrentUserContextType { const CurrentUserContext = createContext(null); const useCurrentUser = () => { - const currentUserContext = useContext(CurrentUserContext); + const currentUserContext = use(CurrentUserContext); if (!currentUserContext) { throw new Error( - "useCurrentUser has to be used within " + "useCurrentUser has to be used within " ); } @@ -1465,8 +1351,6 @@ const useCurrentUser = () => { Using a runtime type check in this will have the benefit of printing a clear error message in the console when a provider is not wrapping the components properly. Now it's possible to access `currentUser.username` without checking for `null`: ```tsx -import { useContext } from "react"; - const MyComponent = () => { const currentUser = useCurrentUser(); @@ -1479,10 +1363,10 @@ const MyComponent = () => { Another way to avoid having to check for `null` is to use type assertion to tell TypeScript you know the context is not `null`: ```tsx -import { useContext } from "react"; +import { use } from "react"; const MyComponent = () => { - const currentUser = useContext(CurrentUserContext); + const currentUser = use(CurrentUserContext); return

Name: {currentUser!.username}.

; }; @@ -1904,9 +1788,154 @@ export default ErrorBoundary; -#### Concurrent React/React Suspense +#### Concurrent React + +The Concurrent React APIs (`Suspense`, `useTransition`, `useDeferredValue`, `startTransition`, `use`) let you keep the UI responsive while React renders work in the background or waits for data. They're all stable as of React 18 and gained additional capabilities in React 19. + +#### `Suspense` + +`Suspense` lets you declaratively show a fallback while a child component is waiting for something — typically data unwrapped with `use(promise)`, a lazy component, or a streamed boundary on the server. + +```tsx +import { Suspense } from "react"; + +const UserProfile = ({ userPromise }: { userPromise: Promise }) => { + const user = use(userPromise); + return

Hello, {user.name}!

; +}; + +const App = ({ userPromise }: { userPromise: Promise }) => ( + Loading...

}> + +
+); +``` + +`SuspenseProps` is typed as `{ children?: ReactNode; fallback?: ReactNode }`. The fallback can be any `ReactNode`, including `null`. + +#### `use` + +`use` reads the value of a context or a promise. Unlike `useContext`, it can be called inside conditions and loops, and it integrates with `Suspense` for promises. + +```tsx +import { use } from "react"; + +const Comments = ({ + commentsPromise, +}: { + commentsPromise: Promise; +}) => { + // Suspends until the promise resolves; throws to the nearest . + const comments = use(commentsPromise); + return ( +
    + {comments.map((c) => ( +
  • {c.text}
  • + ))} +
+ ); +}; +``` + +The promise is typically created by a parent and passed down — don't create it inside the component, or you'll create a new promise on every render. + +#### `useTransition` + +`useTransition` marks a state update as non-urgent so React can keep typing, scrolling, and other urgent input responsive while it renders. + +```tsx +import { useState, useTransition } from "react"; + +const TabSwitcher = () => { + const [isPending, startTransition] = useTransition(); + const [tab, setTab] = useState<"posts" | "comments">("posts"); + + const selectTab = (next: "posts" | "comments") => { + startTransition(() => { + setTab(next); + }); + }; + + return ( + <> + + + {tab === "posts" ? : } + + ); +}; +``` + +##### Async transitions (React 19) + +In React 19, the function passed to `startTransition` can be async. This is the foundation for Actions and is how `useActionState` and `
` schedule their pending state. + +```tsx +const [isPending, startTransition] = useTransition(); + +const onSubmit = () => { + startTransition(async () => { + await saveDraft(content); + setSavedAt(new Date()); + }); +}; +``` + +`isPending` stays `true` for the entire duration of the async callback, including awaited work. + +#### `useDeferredValue` + +`useDeferredValue` lets you defer re-rendering a part of the UI that's expensive to compute, so urgent updates (typing into an input) can flush first. + +```tsx +import { useDeferredValue, useState } from "react"; + +const SearchPage = () => { + const [query, setQuery] = useState(""); + const deferredQuery = useDeferredValue(query); + + return ( + <> + setQuery(e.target.value)} /> + {/* SearchResults re-renders with deferredQuery, lagging behind input */} + + + ); +}; +``` + +##### `initialValue` (React 19) + +React 19 added an optional second argument: the value to use during the initial render before the deferred value has caught up. Useful for SSR/streaming when you want to show a known initial value rather than the latest one. + +```tsx +const deferredQuery = useDeferredValue(query, ""); +``` + +#### `startTransition` (standalone) + +`startTransition` is also exported directly from `react` for use outside components — for example, inside event handlers in non-React code or third-party stores. + +```tsx +import { startTransition } from "react"; + +store.subscribe(() => { + startTransition(() => { + forceRender(); + }); +}); +``` + +The standalone version does not provide an `isPending` flag — use the hook if you need that. + +#### See also -_Not written yet._ watch [https://github.com/sw-yx/fresh-async-react](https://github.com/sw-yx/fresh-async-react) for more on React Suspense and Time Slicing. +- [`useActionState`, `useFormStatus`, `useOptimistic`](https://react.dev/reference/react) — built on top of transitions +- [Server Components and `'use server'`](https://react.dev/reference/rsc/server-components) [Something to add? File an issue](https://github.com/typescript-cheatsheets/react/issues/new). diff --git a/docs/advanced/patterns_by_usecase.md b/docs/advanced/patterns_by_usecase.md index 0a8ada803..9127415e6 100644 --- a/docs/advanced/patterns_by_usecase.md +++ b/docs/advanced/patterns_by_usecase.md @@ -153,20 +153,22 @@ export const defaultProps = < _thanks [dmisdm](https://github.com/typescript-cheatsheets/react/issues/23)_ -:new: You should also consider whether to explicitly forward refs: +You should also consider whether to accept a `ref`. In React 19, `ref` is a regular prop on function components — no `forwardRef` needed: ```tsx -import { forwardRef, ReactNode } from "react"; +import { Ref, ReactNode } from "react"; -// base button, with ref forwarding -type Props = { children: ReactNode; type: "submit" | "button" }; -export type Ref = HTMLButtonElement; +type Props = { + children: ReactNode; + type: "submit" | "button"; + ref?: Ref; +}; -export const FancyButton = forwardRef((props, ref) => ( - -)); +); ``` ## Polymorphic Components (e.g. with `as` props) diff --git a/docs/basic/getting-started/concurrent.md b/docs/basic/getting-started/concurrent.md index 25b9a4c9d..07e4f6bb4 100644 --- a/docs/basic/getting-started/concurrent.md +++ b/docs/basic/getting-started/concurrent.md @@ -1,8 +1,153 @@ --- id: concurrent -title: Concurrent React/React Suspense +title: Concurrent React --- -_Not written yet._ watch [https://github.com/sw-yx/fresh-async-react](https://github.com/sw-yx/fresh-async-react) for more on React Suspense and Time Slicing. +The Concurrent React APIs (`Suspense`, `useTransition`, `useDeferredValue`, `startTransition`, `use`) let you keep the UI responsive while React renders work in the background or waits for data. They're all stable as of React 18 and gained additional capabilities in React 19. + +## `Suspense` + +`Suspense` lets you declaratively show a fallback while a child component is waiting for something — typically data unwrapped with `use(promise)`, a lazy component, or a streamed boundary on the server. + +```tsx +import { Suspense } from "react"; + +const UserProfile = ({ userPromise }: { userPromise: Promise }) => { + const user = use(userPromise); + return

Hello, {user.name}!

; +}; + +const App = ({ userPromise }: { userPromise: Promise }) => ( + Loading...

}> + +
+); +``` + +`SuspenseProps` is typed as `{ children?: ReactNode; fallback?: ReactNode }`. The fallback can be any `ReactNode`, including `null`. + +## `use` + +`use` reads the value of a context or a promise. Unlike `useContext`, it can be called inside conditions and loops, and it integrates with `Suspense` for promises. + +```tsx +import { use } from "react"; + +const Comments = ({ + commentsPromise, +}: { + commentsPromise: Promise; +}) => { + // Suspends until the promise resolves; throws to the nearest . + const comments = use(commentsPromise); + return ( +
    + {comments.map((c) => ( +
  • {c.text}
  • + ))} +
+ ); +}; +``` + +The promise is typically created by a parent and passed down — don't create it inside the component, or you'll create a new promise on every render. + +## `useTransition` + +`useTransition` marks a state update as non-urgent so React can keep typing, scrolling, and other urgent input responsive while it renders. + +```tsx +import { useState, useTransition } from "react"; + +const TabSwitcher = () => { + const [isPending, startTransition] = useTransition(); + const [tab, setTab] = useState<"posts" | "comments">("posts"); + + const selectTab = (next: "posts" | "comments") => { + startTransition(() => { + setTab(next); + }); + }; + + return ( + <> + + + {tab === "posts" ? : } + + ); +}; +``` + +### Async transitions (React 19) + +In React 19, the function passed to `startTransition` can be async. This is the foundation for Actions and is how `useActionState` and `` schedule their pending state. + +```tsx +const [isPending, startTransition] = useTransition(); + +const onSubmit = () => { + startTransition(async () => { + await saveDraft(content); + setSavedAt(new Date()); + }); +}; +``` + +`isPending` stays `true` for the entire duration of the async callback, including awaited work. + +## `useDeferredValue` + +`useDeferredValue` lets you defer re-rendering a part of the UI that's expensive to compute, so urgent updates (typing into an input) can flush first. + +```tsx +import { useDeferredValue, useState } from "react"; + +const SearchPage = () => { + const [query, setQuery] = useState(""); + const deferredQuery = useDeferredValue(query); + + return ( + <> + setQuery(e.target.value)} /> + {/* SearchResults re-renders with deferredQuery, lagging behind input */} + + + ); +}; +``` + +### `initialValue` (React 19) + +React 19 added an optional second argument: the value to use during the initial render before the deferred value has caught up. Useful for SSR/streaming when you want to show a known initial value rather than the latest one. + +```tsx +const deferredQuery = useDeferredValue(query, ""); +``` + +## `startTransition` (standalone) + +`startTransition` is also exported directly from `react` for use outside components — for example, inside event handlers in non-React code or third-party stores. + +```tsx +import { startTransition } from "react"; + +store.subscribe(() => { + startTransition(() => { + forceRender(); + }); +}); +``` + +The standalone version does not provide an `isPending` flag — use the hook if you need that. + +## See also + +- [`useActionState`, `useFormStatus`, `useOptimistic`](https://react.dev/reference/react) — built on top of transitions +- [Server Components and `'use server'`](https://react.dev/reference/rsc/server-components) [Something to add? File an issue](https://github.com/typescript-cheatsheets/react/issues/new). diff --git a/docs/basic/getting-started/context.md b/docs/basic/getting-started/context.md index ef8ca0281..2628ffa40 100644 --- a/docs/basic/getting-started/context.md +++ b/docs/basic/getting-started/context.md @@ -15,7 +15,7 @@ type ThemeContextType = "light" | "dark"; const ThemeContext = createContext("light"); ``` -Wrap the components that need the context with a context provider: +Wrap the components that need the context by rendering the context itself as a provider. In React 19, the context object can be rendered directly — you no longer need ``: ```tsx import { useState } from "react"; @@ -24,25 +24,29 @@ const App = () => { const [theme, setTheme] = useState("light"); return ( - + - + ); }; ``` -Call `useContext` to read and subscribe to the context. +> `` still works and is identical in behavior — it's just the legacy spelling. + +Read the context with `use`: ```tsx -import { useContext } from "react"; +import { use } from "react"; const MyComponent = () => { - const theme = useContext(ThemeContext); + const theme = use(ThemeContext); return

The current theme is {theme}.

; }; ``` +> `useContext(ThemeContext)` still works too. The main difference is that `use` can also unwrap a promise, and it can be called inside conditions and loops. + ## Without default context value If you don't have any meaningful default value, specify `null`: @@ -64,9 +68,9 @@ const App = () => { }); return ( - + - + ); }; ``` @@ -74,10 +78,10 @@ const App = () => { Now that the type of the context can be `null`, you'll notice that you'll get a `'currentUser' is possibly 'null'` TypeScript error if you try to access the `username` property. You can use optional chaining to access `username`: ```tsx -import { useContext } from "react"; +import { use } from "react"; const MyComponent = () => { - const currentUser = useContext(CurrentUserContext); + const currentUser = use(CurrentUserContext); return

Name: {currentUser?.username}.

; }; @@ -86,7 +90,7 @@ const MyComponent = () => { However, it would be preferable to not have to check for `null`, since we know that the context won't be `null`. One way to do that is to provide a custom hook to use the context, where an error is thrown if the context is not provided: ```tsx -import { createContext } from "react"; +import { createContext, use } from "react"; interface CurrentUserContextType { username: string; @@ -95,11 +99,11 @@ interface CurrentUserContextType { const CurrentUserContext = createContext(null); const useCurrentUser = () => { - const currentUserContext = useContext(CurrentUserContext); + const currentUserContext = use(CurrentUserContext); if (!currentUserContext) { throw new Error( - "useCurrentUser has to be used within " + "useCurrentUser has to be used within " ); } @@ -110,8 +114,6 @@ const useCurrentUser = () => { Using a runtime type check in this will have the benefit of printing a clear error message in the console when a provider is not wrapping the components properly. Now it's possible to access `currentUser.username` without checking for `null`: ```tsx -import { useContext } from "react"; - const MyComponent = () => { const currentUser = useCurrentUser(); @@ -124,10 +126,10 @@ const MyComponent = () => { Another way to avoid having to check for `null` is to use type assertion to tell TypeScript you know the context is not `null`: ```tsx -import { useContext } from "react"; +import { use } from "react"; const MyComponent = () => { - const currentUser = useContext(CurrentUserContext); + const currentUser = use(CurrentUserContext); return

Name: {currentUser!.username}.

; }; diff --git a/docs/basic/getting-started/default-props.md b/docs/basic/getting-started/default-props.md index ba17c7497..b29bbdf91 100644 --- a/docs/basic/getting-started/default-props.md +++ b/docs/basic/getting-started/default-props.md @@ -1,70 +1,40 @@ --- id: default_props -title: Typing defaultProps +title: Typing default props --- -## You May Not Need `defaultProps` +## Function components -As per [this tweet](https://twitter.com/dan_abramov/status/1133878326358171650), defaultProps will eventually be deprecated. You can check the discussions here: - -- [Original tweet](https://twitter.com/hswolff/status/1133759319571345408) -- More info can also be found in [this article](https://medium.com/@matanbobi/react-defaultprops-is-dying-whos-the-contender-443c19d9e7f1) - -The consensus is to use object default values. - -Function Components: +As of React 19, `defaultProps` is **no longer supported on function components**. Use destructuring defaults directly in the parameter list — TypeScript will infer the prop as optional automatically: ```tsx type GreetProps = { age?: number }; -const Greet = ({ age = 21 }: GreetProps) => // etc -``` - -Class Components: - -```tsx -type GreetProps = { - age?: number; +const Greet = ({ age = 21 }: GreetProps) => { + // ... }; - -class Greet extends React.Component { - render() { - const { age = 21 } = this.props; - /*...*/ - } -} - -let el = ; ``` -## Typing `defaultProps` - -Type inference improved greatly for `defaultProps` in [TypeScript 3.0+](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html), although [some edge cases are still problematic](https://github.com/typescript-cheatsheets/react/issues/61). - -**Function Components** +If you prefer to declare defaults separately (for example, to share them across components), pull them into a constant and spread them when destructuring: ```tsx -// using typeof as a shortcut; note that it hoists! -// you can also declare the type of DefaultProps if you choose -// e.g. https://github.com/typescript-cheatsheets/react/issues/415#issuecomment-841223219 -type GreetProps = { age: number } & typeof defaultProps; +type GreetProps = { age?: number }; -const defaultProps = { - age: 21, -}; +const defaultProps = { age: 21 } satisfies GreetProps; -const Greet = (props: GreetProps) => { - // etc +const Greet = ({ age = defaultProps.age }: GreetProps) => { + // ... }; -Greet.defaultProps = defaultProps; ``` -_[See this in TS Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAKjgQwM5wEoFNkGN4BmUEIcARFDvmQNwBQdMAnmFnAOKVYwAKxY6ALxwA3igDmWAFxwAdgFcQAIyxQ4AXzgAyOM1YQCcACZYCyeQBte-VPVwRZqeCbOXrEAXGEi6cCdLgAJgBGABo6dXo6e0d4TixuLzgACjAbGXjuPg9UAEovAD5RXzhKGHkoWTgAHiNgADcCkTScgDpkSTgAeiQFZVVELvVqrrrGiPpMmFaXcytsz2FZtwXbOiA)_ +> Setting `Greet.defaultProps = { age: 21 }` will not work on function components in React 19 — the value is ignored at runtime and `FunctionComponent` no longer types it. + +## Class components -For **Class components**, there are [a couple ways to do it](https://github.com/typescript-cheatsheets/react/pull/103#issuecomment-481061483) (including using the `Pick` utility type) but the recommendation is to "reverse" the props definition: +Class components still support `static defaultProps`. The recommended approach is to type the props with the defaulted keys as required, and let `LibraryManagedAttributes` (applied automatically by JSX) make them optional at the call site: ```tsx -type GreetProps = typeof Greet.defaultProps & { +type GreetProps = { age: number; }; @@ -72,27 +42,28 @@ class Greet extends React.Component { static defaultProps = { age: 21, }; - /*...*/ + + render() { + return
Hello, I am {this.props.age}
; + } } -// Type-checks! No type assertions needed! -let el = ; +// Type-checks — `age` is optional at the call site thanks to defaultProps. +const el = ; ```
React.JSX.LibraryManagedAttributes nuance for library authors -The above implementations work fine for App creators, but sometimes you want to be able to export `GreetProps` so that others can consume it. The problem here is that the way `GreetProps` is defined, `age` is a required prop when it isn't because of `defaultProps`. - -The insight to have here is that [`GreetProps` is the _internal_ contract for your component, not the _external_, consumer facing contract](https://github.com/typescript-cheatsheets/react/issues/66#issuecomment-453878710). You could create a separate type specifically for export, or you could make use of the `React.JSX.LibraryManagedAttributes` utility: +If you export `GreetProps` for consumers, `age` will appear required even though `defaultProps` makes it optional at the call site. `GreetProps` is the _internal_ contract — there's a separate _external_ contract that JSX computes via `React.JSX.LibraryManagedAttributes`. You can compute it explicitly: ```tsx -// internal contract, should not be exported out +// internal contract — don't export type GreetProps = { age: number; }; -class Greet extends Component { +class Greet extends React.Component { static defaultProps = { age: 21 }; } @@ -103,109 +74,7 @@ export type ApparentGreetProps = React.JSX.LibraryManagedAttributes< >; ``` -This will work properly, although hovering over`ApparentGreetProps`may be a little intimidating. You can reduce this boilerplate with the`ComponentProps` utility detailed below. - -
- -## Consuming Props of a Component with defaultProps - -A component with `defaultProps` may seem to have some required props that actually aren't. - -### Problem Statement - -Here's what you want to do: - -```tsx -interface IProps { - name: string; -} -const defaultProps = { - age: 25, -}; -const GreetComponent = ({ name, age }: IProps & typeof defaultProps) => ( -
{`Hello, my name is ${name}, ${age}`}
-); -GreetComponent.defaultProps = defaultProps; - -const TestComponent = (props: React.ComponentProps) => { - return

; -}; - -// Property 'age' is missing in type '{ name: string; }' but required in type '{ age: number; }' -const el = ; -``` - -### Solution - -Define a utility that applies `React.JSX.LibraryManagedAttributes`: - -```tsx -type ComponentProps = T extends - | React.ComponentType - | React.Component - ? React.JSX.LibraryManagedAttributes - : never; - -const TestComponent = (props: ComponentProps) => { - return

; -}; - -// No error -const el = ; -``` - -[_See this in TS Playground_](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAKjgQwM5wEoFNkGN4BmUEIcARFDvmQNwBQdMAnmFnAMImQB2W3MABWJhUAHgAqAPjgBeOOLhYAHjD4ATdNjwwAdJ3ARe-cSyyjg3AlihwB0gD6Yqu-Tz4xzl67cl04cAH44ACkAZQANHQAZYAAjKGQoJgBZZG5kAHMsNQBBGBgoOIBXVTFxABofPzgALjheADdrejoLVSgCPDYASSEIETgAb2r0kCw61AKLDPoAXzpcQ0m4NSxOooAbQWF0OWH-TPG4ACYAVnK6WfpF7mWAcUosGFdDd1k4AApB+uQxysO4LM6r0dnAAGRwZisCAEFZrZCbbb9VAASlk0g+1VEamADUkgwABgAJLAbDYQSogJg-MZwYDoAAkg1GWFmlSZh1mBNmogA9Di8XQUfQHlgni8jLpVustn0BnJpQjZTsWrzeXANsh2gwbstxFhJhK3nIPmAdnUjfw5WIoVgYXBReKuK9+JI0TJpPs4JQYEUoNw4KIABYARjgvN8VwYargADkIIooMQoAslvBSe8JAbns7JTSsDIyAQIBAyOHJDQgA) - -## Misc Discussions and Knowledge - -
-Why does React.FC break defaultProps? - -You can check the discussions here: - -- https://medium.com/@martin_hotell/10-typescript-pro-tips-patterns-with-or-without-react-5799488d6680 -- https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30695 -- https://github.com/typescript-cheatsheets/react/issues/87 - -This is just the current state and may be fixed in future. - -
- -
-TypeScript 2.9 and earlier - -For TypeScript 2.9 and earlier, there's more than one way to do it, but this is the best advice we've yet seen: - -```ts -type Props = Required & { - /* additional props here */ -}; - -export class MyComponent extends React.Component { - static defaultProps = { - foo: "foo", - }; -} -``` - -Our former recommendation used the `Partial type` feature in TypeScript, which means that the current interface will fulfill a partial version on the wrapped interface. In that way we can extend defaultProps without any changes in the types! - -```ts -interface IMyComponentProps { - firstProp?: string; - secondProp: IPerson[]; -} - -export class MyComponent extends React.Component { - public static defaultProps: Partial = { - firstProp: "default", - }; -} -``` - -The problem with this approach is it causes complex issues with the type inference working with `React.JSX.LibraryManagedAttributes`. Basically it causes the compiler to think that when creating a JSX expression with that component, that all of its props are optional. - -[See commentary by @ferdaber here](https://github.com/typescript-cheatsheets/react/issues/57) and [here](https://github.com/typescript-cheatsheets/react/issues/61). +For most apps this isn't needed — only library authors who re-export the props type tend to hit it.
diff --git a/docs/basic/getting-started/hooks.md b/docs/basic/getting-started/hooks.md index f7371876f..ea8bb195e 100644 --- a/docs/basic/getting-started/hooks.md +++ b/docs/basic/getting-started/hooks.md @@ -198,11 +198,11 @@ function DelayedEffect(props: { timerMs: number }) { ## useRef -In TypeScript, `useRef` returns a reference that is either [read-only](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/abd69803c1b710db58d511f4544ec1b70bc9077c/types/react/v16/index.d.ts#L1025-L1039) or [mutable](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/abd69803c1b710db58d511f4544ec1b70bc9077c/types/react/v16/index.d.ts#L1012-L1023), depends on whether your type argument fully covers the initial value or not. Choose one that suits your use case. +`useRef` always returns a `RefObject` in current `@types/react`. An initial value is required, and the returned `.current` is typed based on it. (`MutableRefObject` is deprecated and only kept for backwards compatibility.) ### Option 1: DOM element ref -**[To access a DOM element](https://reactjs.org/docs/refs-and-the-dom.html):** provide only the element type as argument, and use `null` as initial value. In this case, the returned reference will have a read-only `.current` that is managed by React. TypeScript expects you to give this ref to an element's `ref` prop: +**To access a DOM element:** provide the element type as a generic and pass `null` as the initial value. React manages `.current` for you, and TypeScript expects you to pass this ref to an element's `ref` prop: ```tsx function Foo() { @@ -246,21 +246,30 @@ Refs demand specificity - it is not enough to just specify any old `HTMLElement` ### Option 2: Mutable value ref -**[To have a mutable value](https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables):** provide the type you want, and make sure the initial value fully belongs to that type: +**To hold a mutable value across renders without re-rendering on change:** pass the initial value you want — React doesn't manage `.current` for you here, you write to it manually. ```tsx function Foo() { - // Technical-wise, this returns MutableRefObject const intervalRef = useRef(null); - // You manage the ref yourself (that's why it's called MutableRefObject!) useEffect(() => { - intervalRef.current = setInterval(...); - return () => clearInterval(intervalRef.current); + intervalRef.current = window.setInterval(() => { + /* ... */ + }, 1000); + return () => { + if (intervalRef.current !== null) clearInterval(intervalRef.current); + }; }, []); - // The ref is not passed to any element's "ref" prop - return ; + return ( + + ); } ``` @@ -271,19 +280,21 @@ function Foo() { ## useImperativeHandle -Based on this [Stackoverflow answer](https://stackoverflow.com/a/69292925/5415299): +In React 19, `ref` is a regular prop on function components, so `useImperativeHandle` is called with the `ref` prop directly — no `forwardRef` needed. ```tsx // Countdown.tsx +import { useImperativeHandle, Ref } from "react"; -// Define the handle types which will be passed to the forwardRef export type CountdownHandle = { start: () => void; }; -type CountdownProps = {}; +type CountdownProps = { + ref?: Ref; +}; -const Countdown = forwardRef((props, ref) => { +const Countdown = ({ ref }: CountdownProps) => { useImperativeHandle(ref, () => ({ // start() has type inference here start() { @@ -292,12 +303,12 @@ const Countdown = forwardRef((props, ref) => { })); return
Countdown
; -}); +}; ``` ```tsx -// The component uses the Countdown component - +// The component using the Countdown component +import { useEffect, useRef } from "react"; import Countdown, { CountdownHandle } from "./Countdown.tsx"; function App() { @@ -314,9 +325,7 @@ function App() { } ``` -### See also: - -- [Using ForwardRefRenderFunction](https://stackoverflow.com/a/62258685/5415299) +> If you still maintain code that targets React < 19, see the [forwardRef section](./forward-create-ref.md) for the legacy approach using `forwardRef`. ## Custom Hooks diff --git a/docs/basic/setup.md b/docs/basic/setup.md index 101567469..45858e91e 100644 --- a/docs/basic/setup.md +++ b/docs/basic/setup.md @@ -21,6 +21,10 @@ React has documentation for [how to start a new React project](https://react.dev - [Gatsby](https://www.gatsbyjs.com/docs/how-to/custom-configuration/typescript/): `npm init gatsby --ts` - [Expo](https://docs.expo.dev/guides/typescript/): `npx create-expo-app -t with-typescript` +If you just want a client-side single-page app without a framework, [Vite](https://vitejs.dev/) is the most common choice: + +- [Vite](https://vitejs.dev/guide/): `npm create vite@latest my-app -- --template react-ts` + ## Try React and TypeScript online There are some tools that let you run React and TypeScript online, which can be helpful for debugging or making sharable reproductions. diff --git a/docs/hoc/index.md b/docs/hoc/index.md index 8ec5e9e5f..5803667e0 100644 --- a/docs/hoc/index.md +++ b/docs/hoc/index.md @@ -20,8 +20,7 @@ There are a lot of use cases where an HOC is used. For example: Here is a base HOC example you can copy right away: -```jsx - +```tsx type PropsAreEqual

= ( prevProps: Readonly

, nextProps: Readonly

@@ -39,7 +38,6 @@ const withSampleHoC =

( (props: P): React.JSX.Element; displayName: string; } => { - function WithSampleHoc(props: P) { //Do something special to justify the HoC. return component(props) as React.JSX.Element; @@ -47,11 +45,14 @@ const withSampleHoC =

( WithSampleHoc.displayName = `withSampleHoC(${componentName})`; - let wrappedComponent = propsAreEqual === false ? WithSampleHoc : React.memo(WithSampleHoc, propsAreEqual); + let wrappedComponent = + propsAreEqual === false + ? WithSampleHoc + : React.memo(WithSampleHoc, propsAreEqual); //copyStaticProperties(component, wrappedComponent); - return wrappedComponent as typeof WithSampleHoc + return wrappedComponent as typeof WithSampleHoc; }; ``` diff --git a/docs/react-types/CSSProperties.md b/docs/react-types/CSSProperties.md new file mode 100644 index 000000000..94ad90209 --- /dev/null +++ b/docs/react-types/CSSProperties.md @@ -0,0 +1,150 @@ +--- +title: CSSProperties +--- + +`CSSProperties` is the type for inline styles passed via the `style` prop. It extends [`csstype`](https://github.com/frenic/csstype)'s `Properties`, so every standard CSS property is covered with autocompletion and value validation. + +## Parameters + +`CSSProperties` does not take any parameters. + +## Usage + +### Inline styles + +```tsx +function Banner() { + return ( +

Hello
+ ); +} +``` + +### Reusable style objects + +Pull style objects out to share between elements. Annotate with `CSSProperties` so TypeScript checks the values: + +```tsx +import { CSSProperties } from "react"; + +const card: CSSProperties = { + borderRadius: 8, + padding: 16, + boxShadow: "0 1px 3px rgba(0, 0, 0, 0.1)", +}; + +function Card({ children }: { children: ReactNode }) { + return
{children}
; +} +``` + +### Typing a `style` prop on your own component + +If your component forwards a style object, type the prop as `CSSProperties` directly — that's exactly what HTML elements use: + +```tsx +type BoxProps = { + style?: CSSProperties; + children?: ReactNode; +}; + +function Box({ style, children }: BoxProps) { + return
{children}
; +} +``` + +## Values: numbers vs strings + +For length-like properties, **numbers are interpreted as pixels** — React appends `px` automatically. Strings are passed through verbatim. + +```tsx +
// → width: 100px +
// → width: 100% +
// → width: 10rem +``` + +A handful of properties (such as `lineHeight`, `opacity`, `zIndex`, `flexGrow`) are unitless — passing a number leaves it unitless. + +## Vendor prefixes + +Vendor-prefixed properties are written in PascalCase, not with the leading hyphen: + +```tsx +const style: CSSProperties = { + WebkitTransform: "rotate(45deg)", + MozAppearance: "none", +}; +``` + +## Custom properties (CSS variables) + +`CSSProperties` deliberately has no index signature, so writing a CSS custom property triggers a TypeScript error: + +```tsx +// ❌ Type '{ "--accent": string; }' is not assignable to type 'CSSProperties'. +
+``` + +There are three common workarounds. + +### 1. Type assertion (quickest) + +```tsx +
+``` + +Fine for one-off use, but you lose type-checking on the rest of the object. + +### 2. Intersection with an indexed type (recommended) + +Keep type-checking for normal properties while allowing any `--*` key: + +```tsx +type CSSPropertiesWithVars = CSSProperties & { + [key: `--${string}`]: string | number; +}; + +const style: CSSPropertiesWithVars = { + color: "white", + "--accent": "tomato", +}; + +
; +``` + +### 3. Module augmentation (when you have a fixed set of variables) + +If your design system has a known list of CSS variables, augment `CSSProperties` once and get autocomplete everywhere: + +```tsx +// global.d.ts +import "react"; + +declare module "react" { + interface CSSProperties { + "--accent"?: string; + "--spacing"?: string | number; + } +} +``` + +After this, `style={{ "--accent": "tomato" }}` type-checks with no assertion. + +## Typing individual CSS values with `csstype` + +If you want a prop that accepts a single CSS value — e.g. a color or a display value — import the underlying [`csstype`](https://github.com/frenic/csstype) package and use the `Property` namespace: + +```tsx +import type { Property } from "csstype"; + +type BadgeProps = { + color?: Property.Color; // any valid CSS + display?: Property.Display; +}; +``` + +`csstype` is already a transitive dependency of `@types/react`, so no extra install is needed — just import the types. + +## CSS-in-JS libraries + +Most CSS-in-JS libraries (Emotion, styled-components, Stitches, vanilla-extract, …) augment `CSSProperties` themselves to support library-specific features such as nested selectors, pseudo-classes as object keys, or theme tokens. If you see properties like `"&:hover"` accepted as keys, that's a library augmentation, not a feature of `@types/react`. diff --git a/docs/react-types/ComponentProps.md b/docs/react-types/ComponentProps.md index 13c67ac9f..714f59ca5 100644 --- a/docs/react-types/ComponentProps.md +++ b/docs/react-types/ComponentProps.md @@ -2,12 +2,20 @@ title: ComponentProps --- -`ComponentProps` constructs a type with all valid props of an element or inferred props of a component. It's an alias for `ComponentPropsWithRef`. +`ComponentProps` constructs a type with all valid props of an element or inferred props of a component. + +`@types/react` ships three related utilities: + +| Type | What it gives you | +| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ComponentProps` | The props as declared by the component or element. | +| `ComponentPropsWithRef` | Same as `ComponentProps`, plus `ref` for class components. For function components in React 19 the result is identical to `ComponentProps` (since `ref` is already a regular prop). | +| `ComponentPropsWithoutRef` | `ComponentProps` with any `ref` prop stripped out. Useful when you spread props onto a child element and don't want `ref` to leak. | :::note -**React 19+**: `ComponentPropsWithRef` is recommended as refs are now passed as props in function components. (See [forwardRef/createRef](/docs/basic/getting-started/forward_and_create_ref)) +**React 19+:** `ComponentProps` is usually all you need — `ref` is just a regular prop for function components. Reach for `ComponentPropsWithoutRef` when you specifically need to remove `ref` from a spread. -**React ≤18**: Prefer `ComponentPropsWithRef` if ref is forwarded and `ComponentPropsWithoutRef` when ref is not forwarded. +**React ≤18:** Prefer `ComponentPropsWithRef` when refs are forwarded, and `ComponentPropsWithoutRef` when they are not. ::: ## Parameters @@ -28,7 +36,7 @@ interface Props extends ComponentProps<"div"> { } function Component({ className, children, text, ...props }: Props) { - // `props` includes `text` in addition to all valid `div` props + // `props` includes all valid `div` props (minus the ones destructured above) } ``` @@ -49,15 +57,15 @@ type MyType = ComponentProps; // ^? type MyType = Props ``` -#### Infer specific prop type +#### Infer a specific prop type -The type of a specific prop can also be inferred this way. Let's say you are using an `` component from a component library. The component takes a `name` prop that determines what icon is shown. You need to use the type of `name` in your app, but it's not made available by the library. You could create a custom type: +The type of a specific prop can also be inferred. Let's say you are using an `` component from a component library. The component takes a `name` prop that determines what icon is shown. You need to use the type of `name` in your app, but it's not made available by the library. You could create a custom type: ```tsx type IconName = "warning" | "checkmark"; ``` -However, this type is not really reflecting the actual set of icons made available by the library. A better solution is to infer the type: +However, this type doesn't reflect the actual set of icons the library exposes. A better solution is to infer the type by indexing into the inferred props: ```tsx import { Icon } from "component-library"; @@ -65,12 +73,3 @@ import { Icon } from "component-library"; type IconName = ComponentProps["name"]; // ^? type IconName = "warning" | "checkmark" ``` - -You can also use the `Pick` utility type to accomplish the same thing: - -```tsx -import { Icon } from "component-library"; - -type IconName = Pick, "name">; -// ^? type IconName = "warning" | "checkmark" -``` diff --git a/docs/react-types/ReactNode.md b/docs/react-types/ReactNode.md index fe8954f99..960c427cf 100644 --- a/docs/react-types/ReactNode.md +++ b/docs/react-types/ReactNode.md @@ -2,7 +2,18 @@ title: ReactNode --- -`ReactNode` is a type that describes what React can render. +`ReactNode` is a type that describes what React can render. It's a union of every value React accepts as a child: + +- `ReactElement` (the result of JSX, `createElement`, or `cloneElement`) +- `string` +- `number` +- `bigint` +- `boolean` (`true` and `false` render as nothing) +- `null` +- `undefined` +- `Iterable` (so arrays of nodes, but also any iterable) +- `ReactPortal` +- `Promise` (for async Server Components — React unwraps the promise via Suspense) ## Parameters @@ -12,7 +23,7 @@ title: ReactNode ### Typing `children` -The most common use case for `ReactNode` is typing `children`. +The most common use case for `ReactNode` is typing `children`. ```tsx import { ReactNode } from "react"; @@ -26,7 +37,7 @@ function Component({ children }: Props) { } ``` -`` accepts anything that React can render as `children`. Here are some examples: +`` accepts anything that React can render as `children`: ```tsx function Examples() { @@ -37,6 +48,7 @@ function Examples() { Hello {123} + {42n} <>Hello @@ -48,3 +60,69 @@ function Examples() { ); } ``` + +### Async Server Components + +A Server Component can be `async` and return a `Promise`. React unwraps the promise through the nearest `` boundary: + +```tsx +// Server Component +async function UserProfile({ userId }: { userId: string }) { + const user = await fetchUser(userId); + return

{user.name}

; +} + +function Page() { + return ( + Loading…

}> + +
+ ); +} +``` + +A bare `Promise` can also be passed as children directly — useful for streaming patterns where the parent kicks off the work and the child resolves it: + +```tsx +function Page() { + const userPromise = fetchUser(); // Promise + return }>{userPromise}; +} +``` + +## `ReactNode` vs `ReactElement` vs `JSX.Element` + +These three types are often confused because all three appear when you write JSX. They are not interchangeable: + +- **`ReactNode`** is the broadest: anything React can render, including primitives, `null`, arrays, and elements. +- **`ReactElement`** describes only the object produced by JSX or `createElement` — it has `type`, `props`, and `key`. A `string` is _not_ a `ReactElement`. +- **`React.JSX.Element`** is essentially `ReactElement` — what the JSX transform infers for a JSX expression. + +### Use `ReactNode` for `children` + +`ReactNode` is the correct type for any prop that receives children-like content, because a caller might pass a string, an array, or `null`: + +```tsx +type Props = { content: ReactNode }; + + // ✅ string is a ReactNode +hi} /> // ✅ element is a ReactNode + // ✅ null is a ReactNode +``` + +### Don't use `ReactNode` as a function-component return type + +A function component's return type should be what React allows components to _return_, not what it allows them to _receive_. Returning a plain `ReactNode` (which includes `bigint`, `Promise`, etc.) is broader than what TypeScript wants to see from a JSX-rendered component. Let TypeScript infer the return type, or use `React.JSX.Element` / `ReactElement` if you must annotate: + +```tsx +// 👎 too broad, and historically caused issues when used in JSX +const MyComponent = (): ReactNode => "hello"; + +// 👍 let TS infer +const MyComponent = () => "hello"; + +// 👍 explicit +const MyComponent = (): React.JSX.Element => hello; +``` + +[Something to add? File an issue](https://github.com/typescript-cheatsheets/react/issues/new). diff --git a/docs/react-types/Ref.md b/docs/react-types/Ref.md new file mode 100644 index 000000000..ea9b2968a --- /dev/null +++ b/docs/react-types/Ref.md @@ -0,0 +1,120 @@ +--- +title: Ref, RefObject, RefCallback +--- + +`@types/react` ships three closely related ref types. Understanding how they fit together is the key to typing refs correctly in React 19, where `ref` is a regular prop on function components. + +| Type | What it is | When to use | +| ---------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `RefObject` | An object with a `current: T` field. | Return type of `useRef` and `createRef`. Pass it as `ref={…}` to read/write `.current`. | +| `RefCallback` | A function that receives the instance (or `null` on unmount). | Inline `ref={node => …}` callbacks. May return a cleanup function in React 19+. | +| `Ref` | `RefCallback \| RefObject \| null`. | Use this as the **prop type** when accepting a ref from a parent — the parent might pass either form. | + +```ts +interface RefObject { + current: T; +} + +type RefCallback = (instance: T | null) => void | (() => void); + +type Ref = RefCallback | RefObject | null; +``` + +## `RefObject` + +`RefObject` is what `useRef` and `createRef` return. Its `.current` is typed based on the initial value you pass: + +```tsx +import { useRef } from "react"; + +const inputRef = useRef(null); +// ^? RefObject + +const idRef = useRef(0); +// ^? RefObject +``` + +When you pass `null` as the initial value with an explicit generic, React manages `.current` for you — TypeScript types it as `T | null` so you have to null-check before use: + +```tsx +useEffect(() => { + inputRef.current?.focus(); +}, []); +``` + +> `MutableRefObject` still exists in `@types/react` for backwards compatibility but is `@deprecated` — use `RefObject` instead. + +## `RefCallback` + +Callback refs are useful when you need to run code the moment the DOM node is attached or detached. The callback is called with the node when it mounts, and with `null` when it unmounts: + +```tsx +
{ + if (node) console.log("mounted", node); + else console.log("unmounted"); + }} +/> +``` + +### Cleanup function (React 19) + +In React 19, a ref callback can return a cleanup function — React calls it instead of invoking the callback again with `null`. This makes ref callbacks symmetric with `useEffect`: + +```tsx +
{ + const observer = new IntersectionObserver(/* ... */); + observer.observe(node); + return () => observer.disconnect(); + }} +/> +``` + +If your callback returns nothing, React falls back to the old behavior and calls it with `null` on unmount. + +## `Ref` (the union) + +`Ref` is the type you should use when **accepting** a ref as a prop, because a caller can pass either a `RefObject` or a callback. + +```tsx +import { Ref } from "react"; + +type FancyInputProps = { + ref?: Ref; + placeholder?: string; +}; + +function FancyInput({ ref, placeholder }: FancyInputProps) { + return ; +} +``` + +In React 19 this is all you need — `ref` is a regular prop, no `forwardRef` wrapper required. + +### Forwarding a ref to a different element + +If the ref you accept doesn't belong on the root element, you can still pass it down — `Ref` is assignable to any element's `ref` prop as long as `T` matches: + +```tsx +type LabelledInputProps = { + label: string; + ref?: Ref; +}; + +function LabelledInput({ label, ref }: LabelledInputProps) { + return ( + + ); +} +``` + +## Related types + +- **`ForwardedRef`** — the legacy `ref` parameter type passed to a `forwardRef` render function. Only relevant if you still use `forwardRef`; prefer `Ref` on a prop instead. +- **`LegacyRef`** — `@deprecated` alias for `Ref`. String refs are no longer supported. +- **`ComponentRef`** — the ref type accepted by a given component or element, e.g. `ComponentRef<"input">` is `HTMLInputElement`. Useful when you want the ref type without writing it out by hand. +- **`RefAttributes`** — the prop shape `{ ref?: Ref }`. Rarely needed directly; intersected by `ComponentPropsWithRef`. diff --git a/docs/react-types/index.md b/docs/react-types/index.md index 55e76caea..4d86ede51 100644 --- a/docs/react-types/index.md +++ b/docs/react-types/index.md @@ -5,4 +5,6 @@ title: React Types `@types/react` makes some types available that can be very useful. Here's a list in alphabetical order with links to the detailed reference pages. - [`ComponentProps`](/docs/react-types/ComponentProps) +- [`CSSProperties`](/docs/react-types/CSSProperties) - [`ReactNode`](/docs/react-types/ReactNode) +- [`Ref`, `RefObject`, `RefCallback`](/docs/react-types/Ref)