Fix/simplify invariant for ColorSchemeName, align manual typedef (#53397)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53397

This is a runtime behaviour fix and an API change to `Appearance.setColorScheme`, motivated by a user report where providing `'unspecified'` (valid) to this function would trigger an incorrect invariant throw. Furthermore, there is already a [first party use](https://github.com/facebook/react-native/blob/aec35b896053d9372ccdaf67c939b2eb216d3455/packages/react-native/Libraries/Utilities/Appearance.js#L101) where we call `Appearance.setColorScheme('unspecified')`.

**Changes**

- `Appearance.d.ts` (current public API, manual types): Fix `ColorSchemeName` type to include `'unspecified'` value, and narrow to remove nullability — aligning with existing Flow source for this type in `NativeAppearance`.
- `Appearance.js` (implementation): Fix the invariant throw by **removing it**, and instead narrowing the input type to non-nullable. Redundant work in `getState` and `getColorScheme` is removed.

Changelog: [General][Breaking] `Appearance.setColorScheme` no longer accepts a nullable value

Reviewed By: andrewdacenko

Differential Revision: D80705652

fbshipit-source-id: cf221a33447606653050d471ca2d0347ab30db81
This commit is contained in:
Alex Hunt
2025-08-29 11:42:14 -07:00
committed by Facebook GitHub Bot
parent b7e64bea29
commit a4581ecd8b
4 changed files with 16 additions and 29 deletions
+3 -5
View File
@@ -9,7 +9,7 @@
import {NativeEventSubscription} from '../EventEmitter/RCTNativeAppEventEmitter';
type ColorSchemeName = 'light' | 'dark' | null | undefined;
type ColorSchemeName = 'light' | 'dark' | 'unspecified';
export namespace Appearance {
type AppearancePreferences = {
@@ -26,7 +26,7 @@ export namespace Appearance {
*
* Example: `const colorScheme = Appearance.getColorScheme();`
*/
export function getColorScheme(): ColorSchemeName;
export function getColorScheme(): ColorSchemeName | null | undefined;
/**
* Set the color scheme preference. This is useful for overriding the default
@@ -34,9 +34,7 @@ export namespace Appearance {
* appearance of the system UI, only the appearance of the app.
* Only available on iOS 13+ and Android 10+.
*/
export function setColorScheme(
scheme: ColorSchemeName | null | undefined,
): void;
export function setColorScheme(scheme: ColorSchemeName): void;
/**
* Add an event handler that is fired when appearance preferences change.
+5 -17
View File
@@ -14,7 +14,6 @@ import typeof INativeAppearance from './NativeAppearance';
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
import EventEmitter from '../vendor/emitter/EventEmitter';
import invariant from 'invariant';
export type {AppearancePreferences};
@@ -60,7 +59,7 @@ function getState(): $NonMaybeType<typeof lazyState> {
appearanceChanged: [AppearancePreferences],
}>(NativeAppearance).addListener('appearanceChanged', newAppearance => {
state.appearance = {
colorScheme: toColorScheme(newAppearance.colorScheme),
colorScheme: newAppearance.colorScheme,
};
eventEmitter.emit('change', state.appearance);
});
@@ -83,7 +82,7 @@ export function getColorScheme(): ?ColorSchemeName {
// Lazily initialize `state.appearance`. This should only
// happen once because we never reassign a null value to it.
state.appearance = {
colorScheme: toColorScheme(NativeAppearance.getColorScheme()),
colorScheme: NativeAppearance.getColorScheme(),
};
}
colorScheme = state.appearance.colorScheme;
@@ -94,13 +93,13 @@ export function getColorScheme(): ?ColorSchemeName {
/**
* Updates the current color scheme to the supplied value.
*/
export function setColorScheme(colorScheme: ?ColorSchemeName): void {
export function setColorScheme(colorScheme: ColorSchemeName): void {
const state = getState();
const {NativeAppearance} = state;
if (NativeAppearance != null) {
NativeAppearance.setColorScheme(colorScheme ?? 'unspecified');
NativeAppearance.setColorScheme(colorScheme);
state.appearance = {
colorScheme: toColorScheme(NativeAppearance.getColorScheme()),
colorScheme,
};
}
}
@@ -114,14 +113,3 @@ export function addChangeListener(
const {eventEmitter} = getState();
return eventEmitter.addListener('change', listener);
}
/**
* TODO: (hramos) T52919652 Use ?ColorSchemeName once codegen supports union
*/
function toColorScheme(colorScheme: ?string): ?ColorSchemeName {
invariant(
colorScheme === 'dark' || colorScheme === 'light' || colorScheme == null,
"Unrecognized color scheme. Did you mean 'dark', 'light' or null?",
);
return colorScheme;
}
+3 -5
View File
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<b43428758b3aefd30b2bd8a32ec0878a>>
* @generated SignedSource<<0170bc4ced1f6d3d4092d837027cd391>>
*
* This file was generated by scripts/js-api/build-types/index.js.
*/
@@ -4642,9 +4642,7 @@ declare type Separators = {
updateProps: (select: "leading" | "trailing", newProps: Object) => void
}
declare type sequence = typeof sequence
declare function setColorScheme(
colorScheme: ColorSchemeName | null | undefined,
): void
declare function setColorScheme(colorScheme: ColorSchemeName): void
declare function setComponentProviderInstrumentationHook(
hook: ComponentProviderInstrumentationHook,
): void
@@ -5972,7 +5970,7 @@ export {
AppState, // f7097b1b
AppStateEvent, // 80f034c3
AppStateStatus, // 447e5ef2
Appearance, // b23e8105
Appearance, // 00cbaa0a
AutoCapitalize, // c0e857a0
BackHandler, // a9f9bad9
BackPressEventName, // 4620fb76
@@ -136,7 +136,7 @@ const ColorShowcase = (props: {themeName: string}) => (
const ToggleNativeAppearance = () => {
const [nativeColorScheme, setNativeColorScheme] =
useState<ColorSchemeName | null>(null);
useState<ColorSchemeName>('unspecified');
const colorScheme = useColorScheme();
useEffect(() => {
@@ -155,7 +155,10 @@ const ToggleNativeAppearance = () => {
title="Set to dark"
onPress={() => setNativeColorScheme('dark')}
/>
<Button title="Unset" onPress={() => setNativeColorScheme(null)} />
<Button
title="Unset"
onPress={() => setNativeColorScheme('unspecified')}
/>
</View>
);
};