网站搜索

javascript - 如何在 React Native 组件中避免重复调用主题和响应式样式的 Hook?


我想实现的目标: 一个高性能的 React Native 主题管理/切换器。

样式应该可以这样使用:<View style={styles.screen}>,其中 styles 来自我在 styles.ts 文件中定义的样式。在那里,我应该可以访问主题颜色(例如:暗色/亮色)和动态尺寸(宽度、高度),这意味着如果我切换到横屏再切换回来,样式会相应更新。

目前,我有这个实现。然而,在每个组件中,我都必须使用我的特定样式调用这个 Hook const styles=useStyles();。我希望能够避免这样(或者隐藏它),只需在使用之前导入它们,这样我就不必在每个组件中为我导入的每个样式调用这样的 Hook。

App.tsx

import { NavigationContainer, DefaultTheme } from '@react-navigation/native';
import { createStackNavigator, StackScreenProps } from '@react-navigation/stack';
import { Button, View, Text, TouchableOpacity } from 'react-native';
import { useThemeStore, colors } from './themeStore';
import { useStyles } from './styles';

type RootStackParamList = {
  Home: undefined;
  Settings: undefined;
};

const Stack = createStackNavigator<RootStackParamList>();

function HomeScreen({ navigation }: StackScreenProps<RootStackParamList, 'Home'>) {
  const styles = useStyles();
  const toggleTheme = useThemeStore(state => state.toggleTheme);

  return (
    <View style={styles.screen}>
      <View style={[styles.container, { marginTop: 40 }]}>
        <Text style={styles.header}>Home Screen</Text>
        <Text style={styles.text}>Current theme settings</Text>
      </View>
      
      <View style={styles.container}>
        <Button title="Toggle Theme" onPress={toggleTheme} />
        <TouchableOpacity
          style={[styles.button, { marginTop: 16 }]}
          onPress={() => navigation.navigate('Settings')}
        >
          <Text style={styles.buttonText}>Go to Settings</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

function SettingsScreen({ navigation }: StackScreenProps<RootStackParamList, 'Settings'>) {
  const styles = useStyles();
  const theme = useThemeStore(state => state.theme);

  return (
    <View style={styles.screen}>
      <View style={styles.container}>
        <Text style={styles.header}>Settings Screen</Text>
        <Text style={styles.text}>Current Theme: {theme.toUpperCase()}</Text>
        
        <TouchableOpacity
          style={[styles.button, { marginTop: 16 }]}
          onPress={() => navigation.goBack()}
        >
          <Text style={styles.buttonText}>Go Back</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

export default function App() {
  const theme = useThemeStore(state => state.theme);

  const navigationTheme = {
    ...DefaultTheme,
    dark: theme === 'dark',
    colors: {
      ...DefaultTheme.colors,
      ...colors[theme],
    },
  };

  return (
    <NavigationContainer theme={navigationTheme}>
      <Stack.Navigator
        screenOptions={{
          headerStyle: {
            backgroundColor: navigationTheme.colors.card,
          },
          headerTintColor: navigationTheme.colors.text,
        }}
      >
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Settings" component={SettingsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

themeStore.ts

import { MMKVLoader } from 'react-native-mmkv-storage';
import {create} from 'zustand';
import { persist } from 'zustand/middleware';
import { storage } from './storage';

export const storage = new MMKVLoader()
  .withInstanceID('themeStorage')
  .initialize();

export const colors = {
  light: {
    primary: '#007AFF',
    background: '#FFFFFF',
    card: '#FFFFFF',
    text: '#000000',
    border: '#D3D3D3',
    notification: '#FF3B30',
  },
  dark: {
    primary: '#BB86FC',
    background: '#121212',
    card: '#1E1E1E',
    text: '#FFFFFF',
    border: '#383838',
    notification: '#CF6679',
  },
};

interface ThemeState {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

export const useThemeStore = create<ThemeState>()(
  persist(
    (set) => ({
      theme: 'light',
      toggleTheme: () => set((state) => ({
        theme: state.theme === 'light' ? 'dark' : 'light'
      })),
    }),
    {
      name: 'theme-storage',
      storage: {
        getItem: async (name) => {
          const value = storage.getString(name);
          return value ? JSON.parse(value) : null;
        },
        setItem: async (name, value) => {
          storage.setString(name, JSON.stringify(value));
        },
        removeItem: async (name) => {
          storage.removeItem(name);
        },
      },
    }
  )
);

styles.ts

import { useThemeStore } from './themeStore';
import { useDimensionsStore } from './dimensionsStore';
import { StyleSheet } from 'react-native';
import { colors } from './themeStore';

export const useStyles = () => {
  const theme = useThemeStore(state => state.theme);
  const { width, height } = useDimensionsStore();
  const themeColors = colors[theme];

  return StyleSheet.create({
    screen: {
      flex: 1,
      backgroundColor: themeColors.background,
      width,
      height,
    },
    container: {
      padding: 16,
      backgroundColor: themeColors.card,
      borderRadius: 8,
      margin: 8,
    },
    text: {
      color: themeColors.text,
      fontSize: 16,
    },
    header: {
      color: themeColors.primary,
      fontSize: 24,
      fontWeight: 'bold',
    },
    button: {
      backgroundColor: themeColors.primary,
      padding: 12,
      borderRadius: 8,
      alignItems: 'center',
    },
    buttonText: {
      color: themeColors.background,
      fontWeight: 'bold',
    },
  });
};

答案#1

一旦你想根据存储中的深色或浅色模式值动态切换颜色,它需要是一个钩子以触发渲染周期。

我脑海中唯一浮现的就是高阶组件。类似 withStyles(),你可以将它包裹在你的组件周围。但这必须在每一个组件/界面上重复进行。

HOC 看起来大致如下:

export const withStyles = <T,>(WrappedComponent: React.FC<T & styles: StyleSheet.NamedStyles<any>>) => {
return (props: T) => {
    const styles = useStyles()

    // here you pass `styles` as an additional prop into your wrapped component
    return (
        <WrappedComponent {...props} styles={styles} />
    )
  }
}

打字的方式可能不太正确。但有了这个,你可以像这样包装一个组件:

const HomeScreen = withStyles<React.FC<StackScreenProps<RootStackParamList, 'Home'>>>(({ navigation, styles }) {
  // styles gets passed in here as prop

  const toggleTheme = useThemeStore(state => state.toggleTheme);

  return (
    <View style={styles.screen}>
      <View style={[styles.container, { marginTop: 40 }]}>
        <Text style={styles.header}>Home Screen</Text>
        <Text style={styles.text}>Current theme settings</Text>
      </View>
      
      <View style={styles.container}>
        <Button title="Toggle Theme" onPress={toggleTheme} />
        <TouchableOpacity
          style={[styles.button, { marginTop: 16 }]}
          onPress={() => navigation.navigate('Settings')}
        >
          <Text style={styles.buttonText}>Go to Settings</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

答案#2

如果你发现自己经常写类似 <View style={styles.screen}>... 的代码,那么为什么不将其抽象成类似自定义样式组件的东西呢?

示例:

const ViewScreen = ({ children, style, ...props }: ViewScreenProps) => {
  const styles = useStyles();

  return (
    <View style={[styles.screen, style]} {...props}>
      {children}
    </View>
  );
};
function HomeScreen(
  { navigation }: StackScreenProps<RootStackParamList, 'Home'>
) {
  const toggleTheme = useThemeStore(state => state.toggleTheme);

  return (
    <ViewScreen}>
      ...
    </ViewScreen>
  );
}

或者将样式抽象为一个 variant 属性。

示例:

const StyledText = ({ variant, style, ...props }) => {
  const styles = useStyles();
  const textStyle = style[variant];

  return <Text style={[textStyle, style]} {...props} />;
};

示例:

function HomeScreen(
  { navigation }: StackScreenProps<RootStackParamList, 'Home'>
) {
  const toggleTheme = useThemeStore(state => state.toggleTheme);

  return (
    <ViewScreen>
      <ViewContainer>
        <StyledText variant="header">Home Screen</StyledText>
        <StyledText variant="text">Current theme settings</StyledText>
      </ViewContainer>
      
      <ViewContainer>
        <Button title="Toggle Theme" onPress={toggleTheme} />
        <StyledTouchableOpacity
          style={{ marginTop: 16 }}
          onPress={() => navigation.navigate('Settings')}
        >
          <StyledText variant="buttonText">Go to Settings</StyledText>
        </StyledTouchableOpacity>
      </ViewContainer>
    </ViewScreen>
  );
}