Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[react] Types for React 19 #69022

Draft
wants to merge 56 commits into
base: master
Choose a base branch
from
Draft

Conversation

eps1lon
Copy link
Collaborator

@eps1lon eps1lon commented Mar 17, 2024

Changelog

Table of Contents

In recent years, the API surface of @types/react has gotten larger than we'd like.
This makes working with React and TypeScript unnecessarily tricky.
So, we're taking this opportunity to make some small breaking changes to our types, deprecating some and removing others.

We've wanted to make the migration easier - most can be automated with codemods.
Check out the migration guide below for more information.

The runtime changes are far more important since they directly affect the end user of your application.
They'll be detailed in React's release notes.

Try It Out (before React 19.0.0 is released)

The @types/* packages don't support pre-releases.
Instead, you'll have to alias React types packages to other packages.
Modern package managers support this via resolutions or overrides.

Yarn and PNPM:

{
  "dependencies": {
    "@types/react": "npm:types-react@alpha",
    "@types/react-dom": "npm:types-react-dom@alpha"
  },
  "resolutions": {
    "@types/react": "npm:types-react@alpha",
    "@types/react-dom": "npm:types-react-dom@alpha"
  }
}

NPM:

{
  "dependencies": {
    "@types/react": "npm:types-react@alpha",
    "@types/react-dom": "npm:types-react-dom@alpha"
  },
  "overrides": {
    "@types/react": "npm:types-react@alpha",
    "@types/react-dom": "npm:types-react-dom@alpha"
  }
}

The following types packages have alphas under different packages:

  • @types/react -> types-react
  • @types/react-dom -> types-react-dom
  • @types/react-is -> types-react-is
  • @types/react-test-renderer -> types-react-test-renderer
  • @types/scheduler -> types-scheduler
  • @types/use-sync-external-store -> types-use-sync-external-store

Migrating

While we have included some breaking changes, most of them can be resolved with codemods to keep manual work to a minimum.

Quick Explanation

# Run the codemod
npx types-react-codemod@latest preset-19 ./path-to-your-react-ts-files

# If you have a lot of unsound access to element props,
# you can run this additional codemod:
npx types-react-codemod@latest react-element-default-any-props ./path-to-your-react-ts-files

Full Explanation

Almost all of the @types/* packages are already compatible with React 19 types unless they specifically rely on React 18. Therefore it is advised to upgrade all React-related @types/* packages first.

Apply the preset-19 codemod from types-react-codemod in its default configuration via npx types-react-codemod@latest preset-19 ./path-to-your-react-ts-files.
In our experience, this covers most breaking changes.

The largest block of remaining type issues relate to props of React elements now defaulting to unknown instead of any.
If you're focus is on migration instead of soundness, you can use the react-element-default-any-props to resolve a large portion of the breaking changes related to ReactElement.

However, the codemod can't cover every pattern.
You probably have to manually adjust the lines relying on any in element.props either by additional runtime checking or manual casts to any.
You can check out the example migrations done on libraries e.g. MUI or apps e.g. Bluesky to get an idea of the possible patterns.

Breaking changes

This section focuses on breaking changes for the React types.
Some types have been removed, some type parameters have been changed, and useRef has been simplified.

Removed deprecated TypeScript types

Some of the removed have types been moved to more relevant packages, like Validator moving to PropTypes.
Others are no longer needed to describe React's behavior.
Removing them means one less thing to learn.

Codemoddable

Type Codemod Replacement
ReactChild deprecated-react-child React.ReactElement | number | string
ReactFragment deprecated-react-fragment Iterable<React.ReactNode>
ReactNodeArray deprecated-react-node-array ReadonlyArray<React.ReactNode>
ReactText deprecated-react-text number | string
Requireable deprecated-prop-types-types Requireable from prop-types
ValidationMap deprecated-prop-types-types ValidationMap from prop-types
Validator deprecated-prop-types-types Validator from prop-types
VoidFunctionComponent deprecated-void-function-component FunctionComponent
VFC deprecated-void-function-component FC
WeakValidationMap deprecated-prop-types-types WeakValidationMap from prop-types

Not Codemoddable

During our example migrations, these types were not used at all.
If you feel a codemod is missing, it can be tracked in the list of missing React 19 codemods.

Type Replacement
ClassicComponentClass ClassicComponentClass from create-react-class
ClassicComponent ClassicComponent from create-react-class
ClassicElement<Props> ClassicElement<Props, InstanceType<T>> from create-react-class
ComponentSpec ComponentSpec from the create-react-class package
Mixin Mixin from the create-react-class package
ReactChildren typeof React.Children
ReactHTML Either ReactHTML from react-dom-factories or, if you used keyof ReactHTML, use HTMLElementType instead
ReactSVG Either ReactSVG from react-dom-factories or, if you used keyof ReactSVG, use SVGElementType instead
SFCFactory No replacement

JSX Namespace

A long-time request is to remove the global JSX namespace from our types in favor of React.JSX.
This helps prevent pollution of global types which prevents conflicts between different UI libraries that leverage JSX.
This change is codemoddable with scoped-jsx.

You'll now need to wrap module augmentation of the JSX namespace in `declare module "....":

// global.d.ts

+ declare module "react" {
    namespace JSX {
      interface IntrinsicElements {
        "my-element": {
          myElementProps: string;
        };
      }
    }
+ }

The exact module specifier depends on the JSX runtime you specified in the compilerOptions of your tsconfig.json.
For "jsx": "react-jsx" it would be react/jsx-runtime.
For "jsx": "react-jsxdev" it would be react/jsx-dev-runtime.
For "jsx": "react" and "jsx": "preserve" it would be react.

Changes to Type Parameters

useReducer

useReducer now has improved type inference thanks to @mfp22.

However, this required a breaking change where useReducer doesn't accept the full reducer type as a type parameter but instead either needs none (and rely on contextual typing) or needs both the state and action type.

The new best practice is not to pass type arguments to useReducer.

-useReducer<React.Reducer<State, Action>>(reducer)
+useReducer(reducer)

However, this may not work in edge cases where you can explicitly type the state and action, by passing in the Action in a tuple:

-useReducer<React.Reducer<State, Action>>(reducer)
+useReducer<State, [Action]>(reducer)

If you define the reducer inline, we encourage to annotate the function parameters instead:

-useReducer<React.Reducer<State, Action>>((state, action) => state)
+useReducer((state: State, action: Action) => state)

This, of course, is also what you'd also have to do if you move the reducer outside of the useReducer call:

const reducer = (state: State, action: Action) => state;

ReactElement

The props of React elements now default to unknown instead of any if the element is typed as ReactElement. This does not affect you if you pass a type argument to ReactElement:

type Example2 = ReactElement<{ id: string }>["props"];
//   ^? { id: string }

But if you relied on the default, you now have to handle unknown:

type Example = ReactElement["props"];
//   ^? Before, was 'any', now 'unknown'

If you rely on this behavior, use the react-element-default-any-props codemod.
You should only need it if you have a lot of legacy code relying on unsound access of element props.
Element introspection only exists as an escape hatch and you should make it explicit that your props access is unsound via an explicit any.

Component types

Due to the removal of legacy context, forward ref render functions (e.g. (props: P, ref: Ref<T>) => ReactNode) will now be rejected by TypeScript if used as a component type.

This was almost always a bug that needed fixing by wrapping the render function in forwardRef or removing the second ref parameter.

Ref cleanup

Due to the introduction of ref cleanup functions, returning anything else from a ref callback will now be rejected by TypeScript.

The fix is usually to stop using implicit returns e.g.

-<div ref={current => (instance = current)} />
+<div ref={current => {instance = current}} />

The original code returned the instance of the HTMLDivElement and TypeScript wouldn't know if this was supposed to be a cleanup function or if you didn't want to return a cleanup function.

You can codemod this pattern with no-implicit-ref-callback-return

propTypes and defaultProps statics

propTypes are now ignored by React.
However, to ease migration, we just type propTypes as any to ease migration in case these components are a bridge between typed and untyped components.
If we'd remove propTypes entirely, a lot of assignments would cause TypeScript issues.

The same does not apply to defaultProps on function components since not rejecting them during type-checking would cause actual issues at runtime.
Please check out the changelog entry for the removal of defaultProps to learn how to migrate off of defaultProps.

Ref changes

A long-time complaint of how TypeScript and React work has been useRef.
We've changed the types so that useRef now requires an argument.
This significantly simplifies its type signature. It'll now behave more like createContext.

// @ts-expect-error: Expected 1 argument but saw none
useRef();
// Passes
useRef(undefined);
// @ts-expect-error: Expected 1 argument but saw none
createContext();
// Passes
createContext(undefined);

This now also means that all refs are mutable.
You'll no longer hit the issue where you can't mutate a ref because you initialised it with null:

const ref = useRef<number>(null);

// Cannot assign to 'current' because it is a read-only property
ref.current = 1;

MutableRef is now deprecated in favor of a single RefObject type which useRef will always return:

interface RefObject<T> {
  current: T
}

declare function useRef<T>: RefObject<T>

useRef still has a convenience overload for useRef<T>(null) that automatically returns RefObject<T | null>.
To ease migration due to the required argument for useRef, a convenience overload for useRef(undefined) was added that automatically returns RefObject<T | undefined>.

Check out [RFC] Make all refs mutable for prior discussions about this change.

Codemod

When you apply the useRef-required-initial codemod (part of preset-19), all useRef() calls will be converted to useRef(undefined).

Repo stuff, not the changelog

Thanks to @mattpocock for writing the changelog!

Closed issues

Closes #64451
Closes #64896
Closes #64772
Closes #64412
Closes #64920

Reviewer notes

For changes to React runtime APIs, please file them as issues to facebook/react instead. Keep the discussion focused on types.

For changelog review, please use eps1lon#35

CI failure related to "React 19 not found on NPM" is expected. All the other failures need to be gone. I'll extracted as many required changes to other packages as possible to earlier PRs.

Maintainer notes

Before merge:

  • "freeze" React changes on master (Basically just don't merge during subsequent tasks. These tasks are small so this shouldn't be an issue).
  • create fork
  • diff ts5.0 fork

Create React 18 fork with https://github.com/eps1lon/react-types-tools/blob/main/createReact18TypesFork.sh

Backport changes to ts5.0 fork with

git diff master HEAD -- types/react | sed "s|a/types/react|a/types/react/ts5.0|g"  | sed "s|b/types/react|b/types/react/ts5.0|g" > backport.diff; git apply --reject backport.diff

@typescript-bot typescript-bot added this to Needs Author Action in New Pull Request Status Board Mar 17, 2024
@eps1lon eps1lon added pkg: react@19.0.x Discussions related to the release of React 19.0 and removed pkg: react@19.0.0 labels Mar 17, 2024
@eps1lon eps1lon self-assigned this Mar 17, 2024
@eps1lon eps1lon force-pushed the react/19 branch 15 times, most recently from 6f5e24f to bd58fbf Compare March 23, 2024 20:10
@eps1lon eps1lon force-pushed the react/19 branch 4 times, most recently from 47920ab to 51e726f Compare March 26, 2024 20:20
@eps1lon eps1lon changed the title [draft] Opening to test compat [react] Types for React 19 Mar 26, 2024
Sebastian Silbermann and others added 21 commits May 23, 2024 18:28
There's little harm in keeping them around for now.

`defaultProps` are still `never` since assuming these are applied would cause issues in production.
This is somewhat bad because hnrs is by definition not compatible since it treats removed statics as react statics even though they no longer are.

But then the constraints weren't necessary in the first place and more of a "this is what you should pass in here".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg: react@19.0.x Discussions related to the release of React 19.0
Projects
Development

Successfully merging this pull request may close these issues.

None yet

5 participants