react-native-community / discussions-and-proposals

Discussions and proposals related to the main React Native project
https://reactnative.dev
1.69k stars 127 forks source link

Platform specific styles inside StyleSheet #254

Open svbutko opened 4 years ago

svbutko commented 4 years ago

Introduction

Current implementation of theming doesn't allow developers to assign theme-dependant changes inside StyleSheet

Details

React Native has a built-in support for theming which is a great addition itself but at the moment it only gives developers information about what theme device has currently and based on this, developer can decide what styles to use in components which can be done different ways, but it always brings to something similar to this:

render() {
    const colorScheme = Appearance.getColorScheme();

    return (
        <View style={colorScheme === "dark" ? darkStyles.container : lightStyles.container}>
            {children}
        </View>
    );
}

const lightStyles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: Colors.white,
    },
});

const darkStyles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: Colors.backgroundDark,
    },
});

So now in order to have different styles working, a developer needs to manually check current theme and apply the styles, and in case if user has a lot of components it creates a lot of clutter and separates the styles by different constants (dark, light).

Besides, styles could have a lot of properties and in order to avoid copying and pasting them, one need to create one more constant style (i.e. commonStyles) and merge it with dark and light ones.

To “avoid” (mitigate) this comparison and “improve” merging, something like this can be done:



    getThemeStyle(lightStyles, darkStyles) {
        return Appearance.getColorScheme() == "light" ? lightStyles : darkStyles;
    }

    render() {
        const styles = getThemeStyle(lightStyles, darkStyles);

        return (
            <View style={styles.container}>
                {children}
            </View>
        );
    }

const commonStyles = StyleSheet.create({
    container: {
        flex: 1,
    },
});

const lightStyles = StyleSheet.create({
    container: {
        ...commonStyles.container,
        backgroundColor: Colors.white,
    },
});

const darkStyles = StyleSheet.create({
    container: {
        ...commonStyles.container,
        backgroundColor: Colors.backgroundDark,
    },
});

What if instead creating separate styles and merging them, and in most of the cases get rid of Appearance.getColorScheme() == “light" comparison, developers could define their theme styles inside StyleSheet.create similar way we have platform dependant code using Platform.select?

So that it could look something like this:

const styles = StyleSheet.create({
    container: {
        flex: 1,
        ...Appearance.select({
            dark: {
                backgroundColor: Colors.backgroundDark
            },
            light: {
                backgroundColor: Colors.white,
            },
        }),
    },
});

Discussion points

svbutko commented 4 years ago

There is a similar discussion about the same issue but it's focusing on colours rather than styles itself.

And to avoid massive changes in StyleSheet, the before mentioned proposal, under the hood, can simply split and merge styles the same way as described in details and return the according style based on the theme (more like a workaround).

This approach will also work in cases if new themes (e.g high contrast) will be available and won't break the already existed code base.

gwmccull commented 4 years ago

I believe Platform.select is able to work because it can be determined either when the app is first loaded or maybe it's resolved at build time by Metro

Appearance.select would not be able to work the same way since the dark-mode preference could change during runtime. To handle that, you'd probably have to move to calculating your styles within the component

svbutko commented 4 years ago

I believe Platform.select is able to work because it can be determined either when the app is first loaded or maybe it's resolved at build time by Metro

Appearance.select would not be able to work the same way since the dark-mode preference could change during runtime. To handle that, you'd probably have to move to calculating your styles within the component

Probably incorrectly described it in the details. I understand the way it works and I'm mentioning Platform.select only to show the way it looks in the context of applying styles, not the implementation itself but the approach to applying different styles within a style

sahrens commented 4 years ago

Perhaps a new top-level StyleSheet.createThemed?

const themedStyles = StyleSheet.createThemed({
  dark: { ... },
  light: { ... },
  customTheme: { ... },
  shared: { ... },
});

then a generic utility function, and a light/dark theme specific hook, so

function MyComponent {
  const styles = useThemedStyles(themedStyles);
  return <View style={styles.container} />;
}
function MyOtherComponent {
  const customTheme: void | 'customTheme' | 'default' = useCustomTheme();
  const styles = getThemedStyle(customTheme, themedStyles);
  return <View style={styles.container} />;

The custom themes would be merged into (overwriting) the shared styles, and if styles don't exist for a theme (like default), the shared styles would just be used instead.

sahrens commented 4 years ago

Another option that would be much more flexible would be to make it functional:

const themedStyles = StyleSheet.createThemed((theme: 'light' | 'dark') => ({
    container: {
      flex: 1,
      backgroundColor: ThemeColors.surfaceBackground[theme],
    },
});
function MyComponent {
  const styles = useThemedStyles(themedStyles);
  return <View style={styles.container} />;
}

StyleSheet.createThemed could either return the function and useThemedStyle could compute the styles at runtime, or StyleSheet.createThemed could pre-compute the themes for each theme option ahead of time, e.g. in a babel transform or some other mechanism to generate static styles, e.g. for react-native-web.

necolas commented 4 years ago

@sahrens We did something much simpler at Twitter. StyleSheet.create was wrapped in user-space so that it can take a function or an object:

const styles = StyleSheet.create((theme) => ({
  root: {
    backgroundColor: theme.backgroundColor
  }
}));

Then you can use styles as normal. It also made it trivial to codemod existing code that used a static theme as done in the OP, without any changes to function bodies.

The benefit of doing this in user space is it isn't tied to just themes either. You can pass any dynamic data to the styles definitions if you want. There's a small cost on web to doing too much of this, which is runtime duplication of style objects for all the variants.

I wonder if RN could support something similar to how CSS custom properties are used for theming on the web, as this means the values (behind an imported theme object) can be dynamically changed without needing any changes to how styles are defined and without needing to re-render the React tree (on web at least).

elicwhite commented 4 years ago

RN could support something similar to how CSS custom properties are used for theming on the web

@necolas This is how PlatformColor works. Essentially like css variables:

StyleSheet.create({
  root: {
    backgroundColor: PlatformColor('myAppCustomColor') // or use the system ones like Apple's PlatformColor('labelColor')
  }
});

And then myAppCustomColor would be defined at the app level and could be swapped without any JS needing to run.

necolas commented 4 years ago

If you change the underlying values at runtime that could be an approach to theming too. But on web you can more than colors

necolas commented 4 years ago

The way I understand StyleSheet is an API only for defining style objects. It doesn't concern itself with any aspect of the state or environment of the application (layout, theme, platform, network, etc). All that can be built over StyleSheet.

A basic way to work with the current theme (OP request) could be done like this: https://gist.github.com/necolas/5b421ca860ed98eabc5fd2b9bc6d1136

Or you can go wild like this: https://github.com/vitalets/react-native-extended-stylesheet

MoOx commented 4 years ago

The way I currently build themed apps (for light, dark only but code prepared for more theme) is that I build a static stylesheet & another bundle of themed stylesheets that can dynamically switched ("current" available but also all if needed: sometimes you want some components using an "hardcoded theme" (eg: some cards in "dark").

In practise I call a styles object in my components (standard RN styles), and one other themedStyles that comes from a hook useTheme. It's pretty easy to implement, probably not perfect and verbose, but very easy to follow & adjust. The themed styles are defined in a single places & looks like (random examples)

This is a way to do that imo, doesn't really require any new apis in RN. You can do the same kind of implementation to adjust styles to the platform (eg: if you follow apple hig or google materiel design).

A library is probably more suited to handle "platform specific styles".