Skip to content

Instantly share code, notes, and snippets.

@SleeplessByte
Created February 18, 2020 20:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SleeplessByte/b3ed24356e150136160b947e0626c95e to your computer and use it in GitHub Desktop.
Save SleeplessByte/b3ed24356e150136160b947e0626c95e to your computer and use it in GitHub Desktop.
Hooks and Contexts to deal with react-native-appearance. Automatically updates and persists changes.
import React, { useMemo } from "react";
import {
createContext,
useContext,
useState,
useEffect,
useCallback
} from "react";
import {
Appearance,
ColorSchemeName,
AppearancePreferences
} from "react-native-appearance";
// You have to provide this yourself.
// Can be _any_ Promise based storage.
import { insecure } from "../../lib/Storage";
// You have to provide this yourself.
// Should return a string to be used as key with the storage.
import { createStorageKey } from "../core/storage";
interface AppearanceContextValue {
colorScheme: ColorSchemeName;
setColorScheme: SetThemeFunction;
clear: ClearThemeFunction;
}
interface OneChildProps {
children: React.ReactChild;
}
type ThemeName = ColorSchemeName;
type ThemeSource = "native" | "storage" | "default";
type ThemeCallback = (value: {
source: ThemeSource;
scheme: ThemeName;
}) => void;
type ProvideColorScheme = {
source: ThemeSource;
scheme: ThemeName;
};
type SetThemeFunction = (theme: ColorSchemeName) => void;
type ClearThemeFunction = () => void;
const APPEARANCE_STORAGE_KEY = createStorageKey("theme");
const DEFAULT_THEME: Exclude<ThemeName, "no-preference"> = "light";
const NEUTRAL_THEME: ThemeName = "no-preference";
const NOOP = () => {};
const AppearanceContext = createContext<AppearanceContextValue>({
colorScheme: NEUTRAL_THEME,
setColorScheme: () => {},
clear: () => {}
});
export function useColorScheme() {
return useContext(AppearanceContext);
}
/**
* Color Scheme Provider that updates based on the system value or a manual
* switch. As long as the manual switch is not triggered, it will use the system
* values.
*/
export function ColorSchemeProvider({ children }: OneChildProps) {
const contextValue = useProvideColorScheme(APPEARANCE_STORAGE_KEY);
return (
<AppearanceContext.Provider value={contextValue}>
{children}
</AppearanceContext.Provider>
);
}
/**
* Color Scheme Provider that never updates unless the prop "theme" updates.
*
* @param theme the theme to pass down the provider
*/
export function StaticColorSchemeProvider({
theme,
children
}: OneChildProps & { theme: ThemeName }) {
const value = useMemo(
() => ({
colorScheme: ensurePreference(theme),
setColorScheme: NOOP,
clear: NOOP
}),
[theme]
);
return (
<AppearanceContext.Provider value={value}>
{children}
</AppearanceContext.Provider>
);
}
/**
* Color Scheme Provider that updates based on a manual switch. As long as the
* manual switch is not triggered, it will use the defaultTheme prop, initially.
*
* @param defaultTheme the initial value, and value on clear
*/
export function ManualColorSchemeProvider({
defaultTheme,
children
}: OneChildProps & { defaultTheme: "dark" | "light" }) {
const [state, setState] = useState<"light" | "dark" | "no-preference">(
() => defaultTheme
);
const clear = useCallback(() => setState(defaultTheme), [defaultTheme]);
return (
<AppearanceContext.Provider
value={{ colorScheme: state, setColorScheme: setState, clear }}
>
{children}
</AppearanceContext.Provider>
);
}
function useCallbackWithStoredTheme(
storageKey: string,
source: ThemeSource,
callback: ThemeCallback
) {
useEffect(() => {
if (source === "storage") {
return;
}
let stillCareAboutTheValue = true;
insecure.getItem(storageKey).then(
value => {
// Bail out if there is no valid value in the storage
if (value === null || !isValidTheme(value)) {
return;
}
stillCareAboutTheValue &&
callback({
source: "storage",
scheme: value
});
},
() => {}
);
return () => {
stillCareAboutTheValue = false;
};
}, [storageKey, source, callback]);
}
function useCallbackWithNativeTheme(
source: ThemeSource,
callback: ThemeCallback
) {
useEffect(() => {
if (source === "storage") {
return;
}
const onNativeValueChanged = ({ colorScheme }: AppearancePreferences) => {
callback({ source: "native", scheme: colorScheme });
};
const subscription = Appearance.addChangeListener(onNativeValueChanged);
return () => {
subscription.remove();
};
}, [source, callback]);
}
function useStorageCallbacks(
storageKey: string,
callback: ThemeCallback
): [ClearThemeFunction, SetThemeFunction] {
const set = useCallback(
(nextColorScheme: ColorSchemeName) => {
insecure.setItem(storageKey, nextColorScheme).catch(() => {});
callback({ source: "storage", scheme: nextColorScheme });
},
[callback]
);
const clear = useCallback(
() => callback({ source: "default", scheme: "no-preference" }),
[callback]
);
return [clear, set];
}
function useProvideColorScheme(storageKey: string): AppearanceContextValue {
const [colorScheme, setColorSchemeState] = useState<ProvideColorScheme>(
() => ({
source: "default",
scheme: Appearance.getColorScheme()
})
);
useCallbackWithStoredTheme(
storageKey,
colorScheme.source,
setColorSchemeState
);
useCallbackWithNativeTheme(colorScheme.source, setColorSchemeState);
const [clear, set] = useStorageCallbacks(storageKey, setColorSchemeState);
return {
colorScheme: ensurePreference(colorScheme.scheme),
setColorScheme: set,
clear
};
}
function isValidTheme(theme: string): theme is ColorSchemeName {
return ["light", "dark", "no-preference"].includes(theme);
}
function ensurePreference(
theme: ColorSchemeName
): Exclude<ColorSchemeName, "no-preference"> {
if (theme === "no-preference") {
return DEFAULT_THEME;
}
return theme;
}
/**
* The MIT License
*
* Copyright 2020 Derk-Jan Karrenbeld
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment