1

I have the next component:

const App = observer(({injector}: Props) => {
  const preferences = injector.getPreferences().execute()
  const themeStore = useLocalStore(() => ({
    _theme:
      preferences.theme === ETheme.DARK
        ? CombinedDarkTheme
        : CombinedDefaultTheme,
    get theme(): ReactNativePaper.Theme & Theme {
      return this._theme
    },
    set theme(value: ReactNativePaper.Theme & Theme) {
      this._theme = value
    },
  }))
  const onThemeChange = React.useCallback(() => {
    const colorScheme = Appearance.getColorScheme()
    themeStore.theme =
      colorScheme === 'dark' ? CombinedDarkTheme : CombinedDefaultTheme
  }, [themeStore])
  React.useEffect(() => {
    if (preferences.theme === ETheme.SYSTEM) {
      const subscription = Appearance.addChangeListener(onThemeChange)
      return subscription.remove
    }
  }, [onThemeChange, preferences.theme])
  return (
    <PaperProvider theme={themeStore.theme}>
      <NavigationContainer theme={themeStore.theme}>
        // Some views
      </NavigationContainer>
    </PaperProvider>
  )
})

Global MobX store:

export default class PreferencesGateway implements IPreferencesGateway {
  _preferences: Preferences = {
    theme: ETheme.LIGHT,
    signedInUserId: null,
  }

  constructor(private _storage: IStorageGateway) {
    makeObservable(this, {
      _preferences: observable,
      preferences: computed,
      setPreferences: action,
    })
  }

  get preferences(): Preferences {
    return this._preferences
  }

  async setPreferences({
    theme,
    signedInUserId,
  }: PreferencesUpdate): Promise<void> {
    if (theme) this._preferences.theme = theme
    if (signedInUserId !== undefined)
      this._preferences.signedInUserId = signedInUserId
    await this.savePersistData()
  }

  async loadPersistData(): Promise<Preferences> {
    const prefs = await this._storage.get<Preferences>(STORAGE_KEY_PREFERENCES)
    if (prefs) this._preferences = prefs
    return this._preferences
  }

  async savePersistData(): Promise<void> {
    await this._storage.set(STORAGE_KEY_PREFERENCES, this._preferences)
  }
}

I need to App listened MobX global store (preferences.theme), the ETheme definition is below:

enum ETheme {
  DARK = 'DARK',
  LIGHT = 'LIGHT',
  SYSTEM = 'SYSTEM',
}

When the user sets preference.theme to DARK or LIGHT, the app just follows a simple expression:

theme = preferences.theme === ETheme.DARK ? ETheme.DARK : ETheme.LIGHT

However, when user sets preference.theme to AUTO, the app has to subscribe on Appearance.addChangeListener() and follows a system theme.

Unfortunately, my solution doesn't work correctly and leads to multiple changes when the user sets SYSTEM. I am a noobie in React Native and MobX and can't get an error reason but seems these stores conflict. I've also tried useState instead of useLocalStore but it just led to infinite redrawing.

Screenshot

How to fix it?

P.S. I feel that I've written a some bul****, and it can be simplified also.

UPD
I simplified App a little:

const App = observer(({injector}: Props) => {
  const preferences = injector.getPreferences().execute()
  const [systemTheme, setSystemTheme] = React.useState<
    ReactNativePaper.Theme & Theme
  >(
    Appearance.getColorScheme() === 'dark'
      ? CombinedDarkTheme
      : CombinedDefaultTheme,
  )
  const onThemeChange = React.useCallback(() => {
    const colorScheme = Appearance.getColorScheme()
    setSystemTheme(
      colorScheme === 'dark' ? CombinedDarkTheme : CombinedDefaultTheme,
    )
  }, [])
  React.useEffect(() => {
    if (preferences.theme === ETheme.SYSTEM) {
      const subscription = Appearance.addChangeListener(onThemeChange)
      return subscription.remove
    }
  }, [onThemeChange, preferences.theme])
  const localTheme =
    preferences.theme === ETheme.DARK ? CombinedDarkTheme : CombinedDefaultTheme
  const theme = preferences.theme === ETheme.SYSTEM ? systemTheme : localTheme
  return (
    <PaperProvider theme={theme}>
      <NavigationContainer theme={theme}>
        <Stack.Navigator initialRouteName="SplashScreen">
          <Stack.Screen name="MainScreen" options={{headerShown: false}}>
            {props => <MainScreen injector={injector} {...props} />}
          </Stack.Screen>
          <Stack.Screen
            name="SettingsScreen"
            options={{
              header: props => <SimpleAppBar {...props} title="Settings" />,
            }}>
            {props => <SettingsScreen injector={injector} {...props} />}
          </Stack.Screen>
          <Stack.Screen
            name="SignInScreen"
            options={{
              header: props => <SimpleAppBar {...props} title="Account" />,
            }}>
            {props => <SignInScreen injector={injector} {...props} />}
          </Stack.Screen>
          <Stack.Screen
            name="SignUpScreen"
            options={{
              header: props => <SimpleAppBar {...props} title="New account" />,
            }}>
            {props => <SignUpScreen injector={injector} {...props} />}
          </Stack.Screen>
          <Stack.Screen name="SplashScreen" options={{headerShown: false}}>
            {props => <SplashScreen injector={injector} {...props} />}
          </Stack.Screen>
        </Stack.Navigator>
      </NavigationContainer>
    </PaperProvider>
  )
})

However, I got a new error when I try to switch a theme from SYSTEM to LIGHT or DARK:

Screenshot 2

Denis Sologub
  • 7,277
  • 11
  • 56
  • 123

1 Answers1

0

Finally, I could fix all errors. Below is a working solution:

const App = observer(({injector}: Props) => {
  const preferences = injector.getPreferences().execute()
  const [systemTheme, setSystemTheme] = React.useState<
    ReactNativePaper.Theme & Theme
  >(
    Appearance.getColorScheme() === 'dark'
      ? CombinedDarkTheme
      : CombinedDefaultTheme,
  )
  const onThemeChange = React.useCallback(() => {
    const colorScheme = Appearance.getColorScheme()
    setSystemTheme(
      colorScheme === 'dark' ? CombinedDarkTheme : CombinedDefaultTheme,
    )
  }, [])
  React.useEffect(() => {
    if (preferences.theme === ETheme.SYSTEM) {
      const subscription = Appearance.addChangeListener(onThemeChange)
      return () => subscription.remove()
    }
  }, [onThemeChange, preferences.theme])
  const localTheme =
    preferences.theme === ETheme.DARK ? CombinedDarkTheme : CombinedDefaultTheme
  const theme = preferences.theme === ETheme.SYSTEM ? systemTheme : localTheme
  return (
    <PaperProvider theme={theme}>
      <NavigationContainer theme={theme}>
        // Some views
      </NavigationContainer>
    </PaperProvider>
  )
})

There were two fixed:

  1. I returned a wrong callback to unsubscribe from events in useEffect(): I've changed return subscription.remove to return () => subscription.remove(), although I can't get a difference honestly but it works.

  2. I stopped to re-assign a variable from global MobX store to useLocalStore() or useState() variables. Instead of this, I've replaced it by the next condition assignment.

    const localTheme =
    preferences.theme === ETheme.DARK ? CombinedDarkTheme : CombinedDefaultTheme
    const theme = preferences.theme === ETheme.SYSTEM ? systemTheme : localTheme
    

It is not related to my problem in the question but I've also notices a warning from MobX about my global store due to loadPersistData tried to modify data asynchronously, I fixed it rewriting loadPersistData a little:

async loadPersistData(): Promise<Preferences> {
  const prefs = await this._storage.get<Preferences>(STORAGE_KEY_PREFERENCES)
  if (prefs) {
    runInAction(() => {
      this._preferences = prefs
    })
  }
  return this._preferences
}
Denis Sologub
  • 7,277
  • 11
  • 56
  • 123