React Native (or React) listen to the style changes for the dark mode in separate ts modules

Viewed 167

Goal: Implement dark mode in react native application. A little introduction to the system: File structure:

  • Profile.ts
  • ProfileCss.ts
  • constants.ts

In my application in order to separate styles from components, I moved them into .ts files and exported as modules. Example of StyleCss.ts:

import {StyleSheet} from 'react-native';
import {Window, FixedCSS, hsize, wsize} from '../../entities/constants';
    
export default StyleSheet.create({
    loading: {
        backgroundColor: FixedCSS.Colors.MainBackgroundColor,
    }
    ...
});

All style-related constant values stored in constants.ts such as Colors, LineHeights, and etc. Also constants.ts contains a class called FixedCSS which has static properties:

export class FixedCSS {
    static isDark = true;

    static Colors = {
        // Main Colors
        RootColor: FixedCSS.isDark ? '#FF6666' : '#FF6666',
        MainBackgroundColor: FixedCSS.isDark ? '#050505' : '#FFFFFF',
        ...
    }
    ...
}

isDark property of FixedCSS is supposed to be changed with the help of Switch component in Profile.ts

<Switch
    value={darkMode}
    onValueChange={value => {
        this.setState({darkMode: value});
    }}
/>

The problem: How can I change isDark property in FixedCSS class so that it will affect the appearance of components. The most problematic aspect is that StyleCSS.ts files do not reflect to the change of isDark property in FixedCSS class, as they are exported only once when js bundle gets created.

1 Answers

Changing a static property of a class is not going to trigger re-renders the way that it's supposed to. This needs to make use of state somehow. I recommend a theme Context. You could use one context or have separate contexts for controlling isDarkMode and providing the CSS, which is what I am doing here.

In order to export a value only once but have that value stay current, the exported value can be a function rather than a constant. I am making the fixed CSS be a function of isDark and the individual component styles be a function of FixedCSS.

constants.ts

export const createFixedCSS = (isDark: boolean) => ({
  isDark,
  Colors: {
    // Main Colors
    RootColor: isDark ? "#FF6666" : "#FF6666",
    MainBackgroundColor: isDark ? "#050505" : "#FFFFFF"
    ...
  }
  ...
});

export type FixedCSS = ReturnType<typeof createFixedCSS>;

export const IS_DARK_DEFAULT = false;

cssContext.ts

// context to provide the FixedCSS object, which also includes isDark
const FixedCSSContext = createContext(createFixedCSS(IS_DARK_DEFAULT));

// context to provide a function for switching modes
// type Dispatch<SetStateAction<T>> allows for setting based on previous value
type SetModeContextValue = Dispatch<SetStateAction<boolean>>;

// needs an initial value that fits the type signature
const SetModeContext = createContext<SetModeContextValue>(() => {
    throw new Error(
      "cannot set `isDark` outside of a `DarkModeContext` Provider"
    );
});

// combine both contexts into one Provider component
export const CSSProvider: FC<{}> = ({ children }) => {
  // store the isDark state
  const [isDark, setIsDark] = useState(IS_DARK_DEFAULT);

  // update the FixedCSS when isDark changes
  // I'm not sure if memoizing is actually necessary here?
  const FixedCSS = useMemo(() => createFixedCSS(isDark), [isDark]);

  return (
    <SetModeContext.Provider value={setIsDark}>
      <FixedCSSContext.Provider value={FixedCSS}>
        {children}
      </FixedCSSContext.Provider>
    </SetModeContext.Provider>
  );
};

// I prefer to export the hooks rather than the contexts themselves
export const useFixedCSS = () => useContext(FixedCSSContext);
export const useSetMode = () => useContext(SetModeContext);

// helper hook for `StyleCss.ts` files
// takes the makeStyles factory function and turns it into a style object by accessing the  FixedCSS context
export const useStyleFactory = <T extends StyleSheet.NamedStyles<T>>(
  factory: (css: FixedCSS) => T
): T => {
  const FixedCSS = useFixedCSS();
  return useMemo(() => factory(FixedCSS), [FixedCSS, factory]);
};

ProfileCss.ts

export const makeStyles = (FixedCSS: FixedCSS) => StyleSheet.create({
    loading: {
        backgroundColor: FixedCSS.Colors.MainBackgroundColor,
    }
});

Profile.ts

const DarkModeToggle = () => {
  // access isDark from global FixedCSS
  const {isDark} = useFixedCSS();
  
  // access update callback
  const setIsDark = useSetMode();

  return (
    <Switch
      value={isDark}
      onValueChange={setIsDark}
    />
  )
}

const Profile = () => {

  // pass the imported `makeStyles` to the hook and get stateful styles object
  const styles = useStyleFactory(makeStyles);

  return (
    <View>
      <DarkModeToggle/>
      <View style={styles.loading}/>
    </View>
  );
};

CodeSandbox Demo (all in one file)

Related