Skip to content

Commit 0f6e19a

Browse files
huntiefacebook-github-bot
authored andcommitted
Narrow ColorSchemeName, remove 'unspecified' from public API
Summary: Splits the `ColorSchemeName` type to separate output values (`'light' | 'dark'`) from the setter input (`ColorSchemeOverride = 'light' | 'dark' | 'unspecified'`). This resolves a longstanding misalignment between what native returns and what the types promise. This is paired with recent/proposed docs updates: - react/react-native-website#5060 - react/react-native-website#5069 **Detailed changes** `'unspecified'` is only meaningful as an input to `setColorScheme()` (meaning "follow the system default"). Neither iOS nor Android ever returns `'unspecified'` from `getColorScheme()` — when `setColorScheme('unspecified')` is called, the JS layer immediately re-queries `NativeAppearance.getColorScheme()` and caches the resolved system value instead. New public types: - `ColorSchemeName = 'light' | 'dark'` - `ColorSchemeOverride = ColorSchemeName | 'unspecified'` Updated signatures: - `setColorScheme()`: `ColorSchemeOverride` (unchanged values, new named type) **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` now returns `'light' | 'dark' | null` (this was always the case, but is a breaking type change) Differential Revision: D102527387
1 parent d1cdce2 commit 0f6e19a

6 files changed

Lines changed: 76 additions & 38 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: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,21 @@
99
*/
1010

1111
import type {EventSubscription} from '../vendor/emitter/EventEmitter';
12-
import type {AppearancePreferences, ColorSchemeName} from './NativeAppearance';
12+
import type {
13+
AppearancePreferences as NativeAppearancePreferences,
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 {ColorSchemeName, ColorSchemeOverride};
23+
24+
export type AppearancePreferences = {
25+
colorScheme: ?ColorSchemeName,
26+
};
1927

2028
type Appearance = {
2129
colorScheme: ?ColorSchemeName,
@@ -56,7 +64,7 @@ function getState(): NonNullable<typeof lazyState> {
5664
eventEmitter,
5765
};
5866
new NativeEventEmitter<{
59-
appearanceChanged: [AppearancePreferences],
67+
appearanceChanged: [NativeAppearancePreferences],
6068
}>(NativeAppearance).addListener('appearanceChanged', newAppearance => {
6169
state.appearance = {
6270
colorScheme: newAppearance.colorScheme,
@@ -69,9 +77,16 @@ function getState(): NonNullable<typeof lazyState> {
6977
}
7078

7179
/**
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.
80+
* Returns the active color scheme (`'light'` or `'dark'`). This value may
81+
* change at runtime, either at the system level (e.g. scheduled color scheme
82+
* change at sunrise or sunset) or when overridden at the app level via
83+
* `setColorScheme()`.
84+
*
85+
* Prefer `useColorScheme()` in React components.
86+
*
87+
* Notes:
88+
* - `null` will only be returned if the native Appearance module is unavailable
89+
* (out of tree platforms).
7590
*/
7691
export function getColorScheme(): ?ColorSchemeName {
7792
let colorScheme = null;
@@ -91,26 +106,28 @@ export function getColorScheme(): ?ColorSchemeName {
91106
}
92107

93108
/**
94-
* Updates the current color scheme to the supplied value.
109+
* Force the application to always adopt a light or dark interface style. Pass
110+
* `'unspecified'` to reset and follow the system default (removes any
111+
* override). This does not affect the system UI, only the application.
95112
*/
96-
export function setColorScheme(colorScheme: ColorSchemeName): void {
113+
export function setColorScheme(colorScheme: ColorSchemeOverride): void {
97114
const state = getState();
98115
const {NativeAppearance} = state;
99116
if (NativeAppearance != null) {
100117
NativeAppearance.setColorScheme(colorScheme);
101118
state.appearance = {
102-
// When setting to 'unspecified', get the actual system color scheme.
103-
// Fall back to the passed value if getColorScheme() returns null.
104119
colorScheme:
105120
colorScheme === 'unspecified'
106-
? (NativeAppearance.getColorScheme() ?? colorScheme)
121+
? (NativeAppearance.getColorScheme() ?? null)
107122
: colorScheme,
108123
};
109124
}
110125
}
111126

112127
/**
113-
* Add an event handler that is fired when appearance preferences change.
128+
* Subscribe to color scheme changes. The listener receives the new appearance
129+
* preferences whenever the color scheme changes, whether from a system event
130+
* or a call to `setColorScheme()`.
114131
*/
115132
export function addChangeListener(
116133
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: 10 additions & 7 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<<7a366632c3ea4b05d3a7d8bc0f09e961>>
88
*
99
* This file was generated by scripts/js-api/build-types/index.js.
1010
*/
@@ -1629,11 +1629,13 @@ declare namespace Appearance {
16291629
getColorScheme,
16301630
setColorScheme,
16311631
addChangeListener,
1632+
ColorSchemeName,
1633+
ColorSchemeOverride,
16321634
AppearancePreferences,
16331635
}
16341636
}
16351637
declare type AppearancePreferences = {
1636-
colorScheme?: ColorSchemeName
1638+
colorScheme: ColorSchemeName | undefined
16371639
}
16381640
declare type AppParameters = {
16391641
initialProps: {
@@ -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, // c1c1ac70
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)