Skip to content

Commit

Permalink
Appearance: Dedupe colorScheme Validation Logic
Browse files Browse the repository at this point in the history
Summary:
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]

Differential Revision: D61567881
  • Loading branch information
yungsters authored and facebook-github-bot committed Aug 21, 2024
1 parent c82d6f1 commit 0a0bd0d
Show file tree
Hide file tree
Showing 4 changed files with 33 additions and 54 deletions.
59 changes: 22 additions & 37 deletions packages/react-native/Libraries/Utilities/Appearance.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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__) {
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
"
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 6 additions & 11 deletions packages/rn-tester/js/examples/Appearance/AppearanceExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,26 @@
* @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';
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>
Expand Down

0 comments on commit 0a0bd0d

Please sign in to comment.