Skip to content

Commit f9b8d1d

Browse files
huntiefacebook-github-bot
authored andcommitted
Remove 'unspecified' from ColorSchemeName (facebook#56686)
Summary: Removes `'unspecified'` from the return type of `Appearance.getColorScheme()` and `useColorScheme()`, splitting the setter input into a separate `ColorSchemeOverride` type. This resolves a longstanding misalignment between what native returns and what the types promise. **Motivation** `'unspecified'` is only meaningful as an input to `setColorScheme()` — neither iOS nor Android ever returns it from `getColorScheme()`. When `setColorScheme('unspecified')` is called, the JS layer re-queries the native module and caches the resolved system value. After this change: - `Appearance.getColorScheme()` returns `'light' | 'dark'` (no longer `'unspecified'`) - `Appearance.setColorScheme()` receives `'light' | 'dark' | 'unspecified'` Paired with docs updates: - facebook/react-native-website#5060 - facebook/react-native-website#5069 **History of this API** - The TurboModule spec originally typed these methods as plain `string` because codegen didn't support union types (T52919652). - When support landed, D63681874 upgraded to `ColorSchemeName = 'light' | 'dark' | 'unspecified'` — a type-level cleanup that inadvertently widened return types to include `'unspecified'`, a value native never returns. This caused `$FlowFixMe` suppressions across downstream callers. - D80705652 later aligned the `.d.ts` and fixed a bug where `setColorScheme('unspecified')` threw an incorrect invariant. Changelog: [General][Breaking] - `useColorScheme()` no longer returns `'unspecified'` (this was always the case, but is a breaking type change) Reviewed By: cipolleschi Differential Revision: D102527387
1 parent 71a0d5d commit f9b8d1d

6 files changed

Lines changed: 70 additions & 36 deletions

File tree

packages/react-native/Libraries/Utilities/Appearance.d.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99

1010
import {NativeEventSubscription} from '../EventEmitter/RCTNativeAppEventEmitter';
1111

12-
type ColorSchemeName = 'light' | 'dark' | 'unspecified';
12+
type ColorSchemeName = 'light' | 'dark';
13+
14+
type ColorSchemeOverride = ColorSchemeName | 'unspecified';
1315

1416
export namespace Appearance {
1517
type AppearancePreferences = {
@@ -19,33 +21,38 @@ export namespace Appearance {
1921
type AppearanceListener = (preferences: AppearancePreferences) => void;
2022

2123
/**
22-
* Note: Although color scheme is available immediately, it may change at any
23-
* time. Any rendering logic or styles that depend on this should try to call
24-
* this function on every render, rather than caching the value (for example,
25-
* using inline styles rather than setting a value in a `StyleSheet`).
24+
* Returns the active color scheme (`'light'` or `'dark'`). This value may
25+
* change at runtime, either at the system level (e.g. scheduled color scheme
26+
* change at sunrise or sunset) or when overridden at the app level via
27+
* `setColorScheme()`.
28+
*
29+
* Prefer `useColorScheme()` in React components.
2630
*
27-
* Example: `const colorScheme = Appearance.getColorScheme();`
31+
* Notes:
32+
* - `null` will only be returned if the native Appearance module is
33+
* unavailable (out of tree platforms).
2834
*/
2935
export function getColorScheme(): ColorSchemeName | null | undefined;
3036

3137
/**
32-
* Set the color scheme preference. This is useful for overriding the default
33-
* color scheme preference for the app. Note that this will not change the
34-
* appearance of the system UI, only the appearance of the app.
35-
* Only available on iOS 13+ and Android 10+.
38+
* Force the application to always adopt a light or dark interface style. Pass
39+
* `'unspecified'` to reset and follow the system default (removes any
40+
* override). This does not affect the system UI, only the application.
3641
*/
37-
export function setColorScheme(scheme: ColorSchemeName): void;
42+
export function setColorScheme(scheme: ColorSchemeOverride): void;
3843

3944
/**
40-
* Add an event handler that is fired when appearance preferences change.
45+
* Subscribe to color scheme changes. The listener receives the new appearance
46+
* preferences whenever the color scheme changes, whether from a system event
47+
* or a call to `setColorScheme()`.
4148
*/
4249
export function addChangeListener(
4350
listener: AppearanceListener,
4451
): NativeEventSubscription;
4552
}
4653

4754
/**
48-
* A new useColorScheme hook is provided as the preferred way of accessing
49-
* the user's preferred color scheme (e.g. Dark Mode).
55+
* Returns the active color scheme (`'light'` or `'dark'`). Automatically
56+
* re-renders the component when the color scheme changes.
5057
*/
5158
export function useColorScheme(): ColorSchemeName;

packages/react-native/Libraries/Utilities/Appearance.js

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@
99
*/
1010

1111
import type {EventSubscription} from '../vendor/emitter/EventEmitter';
12-
import type {AppearancePreferences, ColorSchemeName} from './NativeAppearance';
12+
import type {
13+
AppearancePreferences,
14+
ColorSchemeName,
15+
ColorSchemeOverride,
16+
} from './NativeAppearance';
1317
import typeof INativeAppearance from './NativeAppearance';
1418

1519
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
1620
import EventEmitter from '../vendor/emitter/EventEmitter';
1721

18-
export type {AppearancePreferences};
22+
export type {AppearancePreferences, ColorSchemeName, ColorSchemeOverride};
1923

2024
type Appearance = {
2125
colorScheme: ?ColorSchemeName,
@@ -69,9 +73,16 @@ function getState(): NonNullable<typeof lazyState> {
6973
}
7074

7175
/**
72-
* Returns the current color scheme preference. This value may change, so the
73-
* value should not be cached without either listening to changes or using
74-
* the `useColorScheme` hook.
76+
* Returns the active color scheme (`'light'` or `'dark'`). This value may
77+
* change at runtime, either at the system level (e.g. scheduled color scheme
78+
* change at sunrise or sunset) or when overridden at the app level via
79+
* `setColorScheme()`.
80+
*
81+
* Prefer `useColorScheme()` in React components.
82+
*
83+
* Notes:
84+
* - `null` will only be returned if the native Appearance module is unavailable
85+
* (out of tree platforms).
7586
*/
7687
export function getColorScheme(): ?ColorSchemeName {
7788
let colorScheme = null;
@@ -91,26 +102,28 @@ export function getColorScheme(): ?ColorSchemeName {
91102
}
92103

93104
/**
94-
* Updates the current color scheme to the supplied value.
105+
* Force the application to always adopt a light or dark interface style. Pass
106+
* `'unspecified'` to reset and follow the system default (removes any
107+
* override). This does not affect the system UI, only the application.
95108
*/
96-
export function setColorScheme(colorScheme: ColorSchemeName): void {
109+
export function setColorScheme(colorScheme: ColorSchemeOverride): void {
97110
const state = getState();
98111
const {NativeAppearance} = state;
99112
if (NativeAppearance != null) {
100113
NativeAppearance.setColorScheme(colorScheme);
101114
state.appearance = {
102-
// When setting to 'unspecified', get the actual system color scheme.
103-
// Fall back to the passed value if getColorScheme() returns null.
104115
colorScheme:
105116
colorScheme === 'unspecified'
106-
? (NativeAppearance.getColorScheme() ?? colorScheme)
117+
? (NativeAppearance.getColorScheme() ?? null)
107118
: colorScheme,
108119
};
109120
}
110121
}
111122

112123
/**
113-
* Add an event handler that is fired when appearance preferences change.
124+
* Subscribe to color scheme changes. The listener receives the new appearance
125+
* preferences whenever the color scheme changes, whether from a system event
126+
* or a call to `setColorScheme()`.
114127
*/
115128
export function addChangeListener(
116129
listener: ({colorScheme: ?ColorSchemeName}) => void,

packages/react-native/Libraries/Utilities/useColorScheme.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ const subscribe = (onStoreChange: () => void) => {
2020
return () => appearanceSubscription.remove();
2121
};
2222

23+
/**
24+
* Returns the active color scheme (`'light'` or `'dark'`). Automatically
25+
* re-renders the component when the color scheme changes.
26+
*
27+
* Notes:
28+
* - `null` will only be returned if the native Appearance module is unavailable
29+
* (out of tree platforms).
30+
*/
2331
export default function useColorScheme(): ?ColorSchemeName {
2432
return useSyncExternalStore(subscribe, getColorScheme);
2533
}

packages/react-native/ReactNativeApi.d.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<1c8637ab03a5fec9d39704d1ae305595>>
7+
* @generated SignedSource<<4950f1efd16fed02b526f83325c8351d>>
88
*
99
* This file was generated by scripts/js-api/build-types/index.js.
1010
*/
@@ -1630,6 +1630,8 @@ declare namespace Appearance {
16301630
setColorScheme,
16311631
addChangeListener,
16321632
AppearancePreferences,
1633+
ColorSchemeName,
1634+
ColorSchemeOverride,
16331635
}
16341636
}
16351637
declare type AppearancePreferences = {
@@ -1856,7 +1858,8 @@ declare namespace CodegenTypes {
18561858
}
18571859
}
18581860
declare type ColorListenerCallback = (value: ColorValue) => unknown
1859-
declare type ColorSchemeName = "dark" | "light" | "unspecified"
1861+
declare type ColorSchemeName = "dark" | "light"
1862+
declare type ColorSchemeOverride = "dark" | "light" | "unspecified"
18601863
declare type ColorValue = ____ColorValue_Internal
18611864
declare type ComponentProvider = () => React.ComponentType<any>
18621865
declare type ComponentProviderInstrumentationHook = (
@@ -4709,7 +4712,7 @@ declare type Separators = {
47094712
updateProps: (select: "leading" | "trailing", newProps: Object) => void
47104713
}
47114714
declare type sequence = typeof sequence
4712-
declare function setColorScheme(colorScheme: ColorSchemeName): void
4715+
declare function setColorScheme(colorScheme: ColorSchemeOverride): void
47134716
declare function setComponentProviderInstrumentationHook(
47144717
hook: ComponentProviderInstrumentationHook,
47154718
): void
@@ -6070,7 +6073,7 @@ export {
60706073
AppState, // 12012be5
60716074
AppStateEvent, // 80f034c3
60726075
AppStateStatus, // 447e5ef2
6073-
Appearance, // 00cbaa0a
6076+
Appearance, // df9545f9
60746077
AutoCapitalize, // c0e857a0
60756078
BackHandler, // f139fc69
60766079
BackPressEventName, // 4620fb76
@@ -6080,7 +6083,7 @@ export {
60806083
ButtonProps, // 0df9cb59
60816084
Clipboard, // 41addb89
60826085
CodegenTypes, // 0b8108a8
6083-
ColorSchemeName, // 31a4350e
6086+
ColorSchemeName, // 6615edd6
60846087
ColorValue, // 98989a8f
60856088
ComponentProvider, // b5c60ddd
60866089
ComponentProviderInstrumentationHook, // 9f640048
@@ -6336,7 +6339,7 @@ export {
63366339
useAnimatedColor, // e3511f81
63376340
useAnimatedValue, // b18adb63
63386341
useAnimatedValueXY, // c7ee2332
6339-
useColorScheme, // c216d6f7
6342+
useColorScheme, // 29a517d5
63406343
usePressability, // b4e21b46
63416344
useWindowDimensions, // bb4b683f
63426345
}

packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@ import type {TurboModule} from '../../../../Libraries/TurboModule/RCTExport';
1212

1313
import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboModuleRegistry';
1414

15-
export type ColorSchemeName = 'light' | 'dark' | 'unspecified';
15+
export type ColorSchemeName = 'light' | 'dark';
16+
17+
export type ColorSchemeOverride = 'light' | 'dark' | 'unspecified';
1618

1719
export type AppearancePreferences = {
1820
colorScheme?: ?ColorSchemeName,
1921
};
2022

2123
export interface Spec extends TurboModule {
2224
+getColorScheme: () => ?ColorSchemeName;
23-
+setColorScheme: (colorScheme: ColorSchemeName) => void;
25+
+setColorScheme: (colorScheme: ColorSchemeOverride) => void;
2426

2527
// RCTEventEmitter
2628
+addListener: (eventName: string) => void;

packages/rn-tester/js/examples/Appearance/AppearanceExample.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {useEffect, useState} from 'react';
1818
import {Appearance, Button, Text, View, useColorScheme} from 'react-native';
1919

2020
function ColorSchemeSubscription() {
21-
const [colorScheme, setColorScheme] = useState<?ColorSchemeName | string>(
21+
const [colorScheme, setColorScheme] = useState<?ColorSchemeName>(
2222
Appearance.getColorScheme(),
2323
);
2424

@@ -135,8 +135,9 @@ const ColorShowcase = (props: {themeName: string}) => (
135135
);
136136

137137
const ToggleNativeAppearance = () => {
138-
const [nativeColorScheme, setNativeColorScheme] =
139-
useState<ColorSchemeName>('unspecified');
138+
const [nativeColorScheme, setNativeColorScheme] = useState<
139+
ColorSchemeName | 'unspecified',
140+
>('unspecified');
140141
const colorScheme = useColorScheme();
141142

142143
useEffect(() => {

0 commit comments

Comments
 (0)