skip to content

After upgrading to React Native 0.87, tsc rejects imports from react-native/Libraries and useRef<View>; what changed, and how do you migrate or opt out?

level: seniorimportance: should knowfreq 34%

answer

  1. Strict TypeScript API default in 0.87
  2. types generated from source, root only
  3. Libraries/* exports have types: null
  4. refs use ViewInstance and friends
  5. react-native-legacy-deep-imports condition

basics

~20 s

React Native 0.87 made the Strict TypeScript API the default: source-generated types scoped to the root react-native export. Deep imports lose their types and refs need *Instance types; migrate, or opt out temporarily through 0.88.

solid answer

~40 s

In 0.87 the **Strict TypeScript API** became the default: React Native's types are now generated from its source and cover only what `react-native` exports at its root. The package's `exports` map gives `./Libraries/*` no types, so deep imports fail `tsc`, and built-in components are typed as functions, so `useRef<View>` no longer names the instance; use `useRef<ViewInstance>`. Other changes: `*Static` types such as `LinkingStatic` are gone, `*Properties` aliases become `*Props`, codegen types come from the root `CodegenTypes` namespace, and `InitializeCore` becomes `react-native/setup-env`. Keep `skipLibCheck` on and update libraries first. As a temporary bridge, add `"react-native-legacy-deep-imports"` to `customConditions`; it stays available through 0.88. Nothing changes at runtime.

code

tsx · 14 lines
tsx
import {useRef} from 'react';
import {TextInput, View} from 'react-native';
import type {TextInputInstance, ViewInstance} from 'react-native';

export function SearchBar() {
  const containerRef = useRef<ViewInstance>(null);
  const inputRef = useRef<TextInputInstance>(null);

  return (
    <View ref={containerRef}>
      <TextInput ref={inputRef} placeholder="Search contacts" />
    </View>
  );
}

go deeper

for a junior

Recall that React Native 0.87 types only the root react-native export and that refs use types such as ViewInstance.

for a middle

Explain the exports-map mechanism: deep paths have no types by default, runtime is unchanged, and customConditions can restore the old types.

for a senior

Plan the migration: update libraries, keep skipLibCheck, stub incompatible library paths, fix refs and removed types, and time-box any opt-out before 0.89.

for a principal

Weigh opting out against migrating during the upgrade, given the opt-out's end date and the long-term benefit of a stable, source-generated API.

## What changed in 0.87 React Native is written in Flow. Its TypeScript types used to be **hand-maintained**, originally from DefinitelyTyped, and every file under `react-native/Libraries/` was reachable. React Native 0.80 introduced the **Strict TypeScript API** as an opt-in; **0.87 makes it the default**. Two things define it: - the types are **generated from React Native's source**, so they match the implementation; - the public API is **only what the root `react-native` entry exports**; internal files are no longer part of it, so moving them is no longer a breaking change. ## How the mechanism works The `react-native` package's `exports` map in 0.87 decides what TypeScript sees: | Import | Default types | With `react-native-legacy-deep-imports` | Runtime (Metro) | |---|---|---|---| | `react-native` | generated types | the old manual types | `index.js` | | `react-native/Libraries/*` | **`null`**, so no types | old per-file `.d.ts` | the JS file still resolves | So a deep import is now a **type error** in `tsc`, typically reported as an unresolvable module, while Metro still bundles the file. The docs' FAQ is explicit that the Strict API does not change runtime. Separately, 0.87 removed `react-native/src/private/*` from the exports, which does affect runtime, and in development `@react-native/babel-preset` still logs a warning for deep imports in your own code. ## The errors you will see, and the fixes 1. **Deep imports**: replace them with root imports. Codegen helpers are now exported from the root: `import {CodegenTypes, codegenNativeComponent} from 'react-native'`, with types such as `CodegenTypes.Int32`. 2. **Ref types**: components are typed as functions, so `useRef<View>(null)` fails. Use the dedicated instance types: `ViewInstance`, `TextInputInstance`, `ScrollViewInstance`, `FlatListInstance` and so on. `React.ComponentRef<typeof View>` remains valid and produces the same type. `Animated.LegacyRef` is gone; a `ViewInstance` ref works for `Animated.View` too. 3. **`*Static` types**: `LinkingStatic`, `AppStateStatic`, `PlatformStatic` and similar are removed; use the value's own name as the type, for example `Linking`. 4. **`*Properties` aliases**: `ViewProperties` becomes `ViewProps`, `TextInputProperties` becomes `TextInputProps`. 5. **Test setup**: `import 'react-native/Libraries/Core/InitializeCore'` becomes `import 'react-native/setup-env'`. Plain `jest.mock('react-native/Libraries/...')` strings keep working, because the path string is not type-checked. 6. **Optional props** are typed as `type | undefined`, which can surface new strictness errors. ## Before you start - **Keep `skipLibCheck` on.** `@react-native/typescript-config` enables it; without it, errors inside dependencies' `.d.ts` files, which you cannot fix, flood the output. - **Update libraries first.** Some libraries ship raw TypeScript, such as Jest setup files, that your project type-checks; several popular ones have released fixes. - **Exclude a stubborn library locally** by mapping its offending subpath to an untyped stub with `paths`, and report the problem upstream. ## Opting out, temporarily If the app cannot migrate yet, restore the previous types in `tsconfig.json`: ```json { "extends": "@react-native/typescript-config", "compilerOptions": { "customConditions": ["react-native", "react-native-legacy-deep-imports"] } } ``` Keep `"react-native"` in the list, because your array replaces the base config's. The 0.87 release post calls this a **temporary bridge**: it remains available **through React Native 0.88**, and the legacy types are intended to be removed in the following release. Treat it as time to migrate, not as a setting. ## Apps and libraries migrate independently The Strict API applies per project, through each project's own `tsconfig.json`. An app can migrate before its libraries, and a library that ships compiled JavaScript with `.d.ts` files does not force anything on its users. The exception is a library that ships raw TypeScript for consumers to import: that source is checked inside the consumer's project and must not use deep imports. ## Expo projects Expo SDK 57 apps run React Native 0.86, where the strict typings were still an opt-in in React Native itself; Expo has enabled them by default in its projects since SDK 54, so many Expo apps already meet these errors before reaching 0.87. ## A migration order that works 1. Upgrade libraries that have released Strict API fixes. 2. Run `tsc` and group errors by kind: deep imports, refs, removed types, optional props. 3. Fix deep imports and refs first; they are the most numerous and the most mechanical. 4. Stub any library that still fails, and report it upstream. 5. Only if the deadline demands it, add the legacy condition, with a ticket to remove it before 0.89.

  • In React Native 0.87, does the react-native-legacy-deep-imports opt-out change what the app runs?
    No. The custom condition only changes which type definitions `tsc` resolves for `react-native` and `react-native/Libraries/*`. Metro resolves the same JavaScript either way. The unrelated removal of `react-native/src/private/*` from the exports is the 0.87 change that does affect runtime.
  • In 0.87, a library's Jest setup file errors with Cannot find module 'react-native/Libraries/...'; what are your options?
    First update the library, since several have shipped fixes. If none exists, map that library subpath in `paths` to an untyped stub declaration so `tsc` skips it, report the issue upstream, and avoid turning off `skipLibCheck`, which would add errors rather than remove them.

saying these in an interview costs you the question

  • The Strict TypeScript API changes what code Metro bundles
  • useRef<View> is still the recommended way to type a ref
  • Turning off skipLibCheck helps find Strict API problems
  • The legacy deep-imports opt-out is a permanent setting
  • Libraries must migrate before apps can adopt the Strict API