Appearance: Dedupe colorScheme Validation Logic (#46120)

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

Currently, the implementation of `Appearance` duplicates the validation logic of string `colorScheme` values multiple times.

This leads to more complicated code and also unnecessary work in certain edge cases (e.g. when `NativeAppearance` is not registered).

This refactors `Appearance` to be simpler and to do less work. I've also configured `NativeAppearance.setColorScheme` to be non-nullable because it has existed since 2023.

Changelog:
[Internal]

Reviewed By: TheSavior

Differential Revision: D61567881

fbshipit-source-id: 61cb51709dc716ad97ae1397105414e74fe57a28
This commit is contained in:
Tim Yung
2024-08-21 01:48:58 -07:00
committed by Facebook GitHub Bot
parent 693a575143
commit ed3ca0730c
4 changed files with 33 additions and 54 deletions
+22 -37
View File
@@ -19,9 +19,8 @@ import NativeAppearance, {
} from './NativeAppearance';
import invariant from 'invariant';
type AppearanceListener = (preferences: AppearancePreferences) => void;
const eventEmitter = new EventEmitter<{
change: [AppearancePreferences],
change: [{colorScheme: ?ColorSchemeName}],
}>();
type NativeAppearanceEventDefinitions = {
@@ -32,24 +31,15 @@ if (NativeAppearance != null) {
new NativeEventEmitter<NativeAppearanceEventDefinitions>(
NativeAppearance,
).addListener('appearanceChanged', (newAppearance: AppearancePreferences) => {
const {colorScheme} = newAppearance;
invariant(
colorScheme === 'dark' || colorScheme === 'light' || colorScheme == null,
"Unrecognized color scheme. Did you mean 'dark' or 'light'?",
);
const colorScheme = toColorScheme(newAppearance.colorScheme);
eventEmitter.emit('change', {colorScheme});
});
}
/**
* Note: Although color scheme is available immediately, it may change at any
* time. Any rendering logic or styles that depend on this should try to call
* this function on every render, rather than caching the value (for example,
* using inline styles rather than setting a value in a `StyleSheet`).
*
* Example: `const colorScheme = Appearance.getColorScheme();`
*
* @returns {?ColorSchemeName} Value for the color scheme preference.
* Returns the current color scheme preference. This value may change, so the
* value should not be cached without either listening to changes or using
* the `useColorScheme` hook.
*/
export function getColorScheme(): ?ColorSchemeName {
if (__DEV__) {
@@ -59,37 +49,32 @@ export function getColorScheme(): ?ColorSchemeName {
return 'light';
}
}
// TODO: (hramos) T52919652 Use ?ColorSchemeName once codegen supports union
const nativeColorScheme: ?string =
NativeAppearance == null ? null : NativeAppearance.getColorScheme() || null;
invariant(
nativeColorScheme === 'dark' ||
nativeColorScheme === 'light' ||
nativeColorScheme == null,
"Unrecognized color scheme. Did you mean 'dark' or 'light'?",
);
return nativeColorScheme;
return toColorScheme(NativeAppearance?.getColorScheme());
}
/**
* Updates the current color scheme to the supplied value.
*/
export function setColorScheme(colorScheme: ?ColorSchemeName): void {
const nativeColorScheme = colorScheme == null ? 'unspecified' : colorScheme;
invariant(
colorScheme === 'dark' || colorScheme === 'light' || colorScheme == null,
"Unrecognized color scheme. Did you mean 'dark', 'light' or null?",
);
if (NativeAppearance != null && NativeAppearance.setColorScheme != null) {
NativeAppearance.setColorScheme(nativeColorScheme);
}
NativeAppearance?.setColorScheme(toColorScheme(colorScheme) ?? 'unspecified');
}
/**
* Add an event handler that is fired when appearance preferences change.
*/
export function addChangeListener(
listener: AppearanceListener,
listener: ({colorScheme: ?ColorSchemeName}) => void,
): EventSubscription {
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;
}
@@ -9155,11 +9155,10 @@ declare export default typeof UTFSequence;
`;
exports[`public API should not change unintentionally Libraries/Utilities/Appearance.js 1`] = `
"type AppearanceListener = (preferences: AppearancePreferences) => void;
declare export function getColorScheme(): ?ColorSchemeName;
"declare export function getColorScheme(): ?ColorSchemeName;
declare export function setColorScheme(colorScheme: ?ColorSchemeName): void;
declare export function addChangeListener(
listener: AppearanceListener
listener: ({ colorScheme: ?ColorSchemeName }) => void
): EventSubscription;
"
`;
@@ -14,19 +14,19 @@ import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboMod
export type ColorSchemeName = 'light' | 'dark';
export type AppearancePreferences = {|
export type AppearancePreferences = {
// TODO: (hramos) T52919652 Use ?ColorSchemeName once codegen supports union
// types.
/* 'light' | 'dark' */
colorScheme?: ?string,
|};
};
export interface Spec extends TurboModule {
// TODO: (hramos) T52919652 Use ?ColorSchemeName once codegen supports union
// types.
/* 'light' | 'dark' */
+getColorScheme: () => ?string;
+setColorScheme?: (colorScheme: string) => void;
+setColorScheme: (colorScheme: string) => void;
// RCTEventEmitter
+addListener: (eventName: string) => void;
@@ -8,10 +8,7 @@
* @flow
*/
import type {
AppearancePreferences,
ColorSchemeName,
} from 'react-native/Libraries/Utilities/NativeAppearance';
import type {ColorSchemeName} from 'react-native/Libraries/Utilities/NativeAppearance';
import {RNTesterThemeContext, themes} from '../../components/RNTesterTheme';
import * as React from 'react';
@@ -19,20 +16,18 @@ import {useEffect, useState} from 'react';
import {Appearance, Button, Text, View, useColorScheme} from 'react-native';
function ColorSchemeSubscription() {
const [colorScheme, setScheme] = useState<?ColorSchemeName | string>(
const [colorScheme, setColorScheme] = useState<?ColorSchemeName | string>(
Appearance.getColorScheme(),
);
useEffect(() => {
const subscription = Appearance.addChangeListener(
(preferences: AppearancePreferences) => {
const {colorScheme: scheme} = preferences;
setScheme(scheme);
({colorScheme: newColorScheme}: {colorScheme: ?ColorSchemeName}) => {
setColorScheme(newColorScheme);
},
);
return () => subscription?.remove();
}, [setScheme]);
return () => subscription.remove();
}, [setColorScheme]);
return (
<RNTesterThemeContext.Consumer>