How to combine StyleSheet with prop-driven styles and themes? #1801
Replies: 24 comments 3 replies
-
I was using a lot of StyleSheet objects created ahead, but DX wasn't ideal. export const useStyles = <T extends NamedStyles<T> | NamedStyles<any>>(
create: (theme: Theme) => T | NamedStyles<T>,
deps: DependencyList = [],
): T => {
const theme = useTheme();
// I wish exhaustive-deps could check also custom hooks somehow.
// eslint-disable-next-line react-hooks/exhaustive-deps
return useMemo(() => StyleSheet.create(create(theme)), [
// eslint-disable-next-line react-hooks/exhaustive-deps
...deps,
theme,
]);
};
const styles = useStyles((theme) => ({
text: {
marginVertical: theme.spacing.smaller,
},
})); |
Beta Was this translation helpful? Give feedback.
-
my approach is to inline those variables coming from theme. use static styles as much as possible for optimization, and use dynamic styles when needed. |
Beta Was this translation helpful? Give feedback.
-
@minheq Inlining is an option though. 🤔 By the way, if I get it right, styles compilation uses some kind of cache. Maybe performance implications of What confuses me is poor understanding what will happen if we have, for instance, 10 Would be nice to hear a comment from @necolas |
Beta Was this translation helpful? Give feedback.
-
Found an interesting discussion in the RN Community react-native-community/discussions-and-proposals#254 And specifically this comment: react-native-community/discussions-and-proposals#254 (comment) |
Beta Was this translation helpful? Give feedback.
-
@ElForastero thanks for linking to that comment. I actually made use of that and implemented what was described in it in Typescript: import {
Appearance,
ColorSchemeName,
StyleSheet,
ViewStyle,
TextStyle,
ImageStyle,
} from 'react-native';
import { isNullish } from '../../lib/js_utils';
import { Theme, lightTheme, darkTheme } from './theme';
// From https://gist.github.com/necolas/5b421ca860ed98eabc5fd2b9bc6d1136
export const DynamicStyleSheet = {
create: <T extends NamedStyles<T> | NamedStyles<any>>(
styles: Styles<T>,
): T => {
const cache = new Map<ColorSchemeName, T>();
const theme = Appearance.getColorScheme();
const stylesObject = getStyles(
styles,
theme === 'dark' ? darkTheme : lightTheme,
);
cache.set(theme, stylesObject);
const dynamicStyles: {
[key: string]: ViewStyle | TextStyle | ImageStyle;
} = {};
for (const key in stylesObject) {
Object.defineProperty(dynamicStyles, key, {
enumerable: true,
get() {
const _theme = Appearance.getColorScheme();
if (!cache.has(_theme)) {
cache.set(
_theme,
getStyles(styles, _theme === 'dark' ? darkTheme : lightTheme),
);
}
const styleObject = cache.get(_theme);
if (isNullish(styleObject)) {
throw new Error(
`Could not get styleObject from cache ${JSON.stringify(cache)}`,
);
}
return styleObject[key];
},
});
}
return dynamicStyles as T;
},
};
type NamedStyles<T> = {
[P in keyof T]: ViewStyle | TextStyle | ImageStyle;
};
type Styles<T extends NamedStyles<T>> = ((theme: Theme) => T) | T;
function getStyles<T extends NamedStyles<T>>(
styles: Styles<T>,
theme: Theme,
): T {
return StyleSheet.create(
typeof styles === 'function' ? styles(theme) : styles,
);
} and could use it as such: const styles = DynamicStyleSheet.create((theme) => ({
root: {
backgroundColor: theme.backgroundColor
}
})); |
Beta Was this translation helpful? Give feedback.
-
@minheq thanks for sharing that example. Any shot you could explain what it's doing a little bit to help performance? It's not super obvious to me, so I'm probably missing something. Are you putting the dynamic style sheet into some sort of hook that passes the theme down to it, or just getting the theme when the app first loads with
@steida You could add |
Beta Was this translation helpful? Give feedback.
-
@nandorojo I have not measured the performance of this approach so I can't really say much about it. I'm taking this approach mostly because of the ergonomy and convenience. What I know is that changing theme will not cause any re-rendering. The style values are evaluated during each component render -- and so maybe the cost is here. |
Beta Was this translation helpful? Give feedback.
-
@minheq It the same as creating styles in render. Except it does not rerender so you have to rerender app manually. Styles depending on props must be created in the render, there in no other way. |
Beta Was this translation helpful? Give feedback.
-
@steida in the code implemented above in For styles that depend on props, my approach is to create style for each variation: const Button = ({ color }) => {
return <View style={[styles.common, styles[color]]} />
}
// Create the style object, like regular StyleSheet
const styles = DynamicStyleSheet.create((theme) => ({
common: {
height: 40,
},
blue: {
backgroundColor: theme.blue,
},
red: {
backgroundColor: theme.red,
}
})); |
Beta Was this translation helpful? Give feedback.
-
@minheq I see "getStyles" in getter. It seems to be evaluated when styles are consumed by RNfW. |
Beta Was this translation helpful? Give feedback.
-
@steida that's correct. however, notice that the style objects are cached per theme. so without caching -- yes, styles are created per render. |
Beta Was this translation helpful? Give feedback.
-
Well, wouldn't a solution such as styled-components or rn-css be the best fit for that? |
Beta Was this translation helpful? Give feedback.
-
@Sharcoux The main issue with them is that they add a one more level of runtime overhead and potential compatibility issues for web. In the end of the day we have only |
Beta Was this translation helpful? Give feedback.
-
I'm using rn-css on a full cross-platform project (iOS, Android, web, MacOS, Linux, Windows). I had no issue with it so far. I don't see what you mean by a runtime overhead. Do you have a concrete example? |
Beta Was this translation helpful? Give feedback.
-
@Sharcoux do you use some babel plugins to preprocess tagged templates? Otherwise their parsing will be done in runtime. Correct me if I'm wrong. |
Beta Was this translation helpful? Give feedback.
-
Well, javascript is a scripting language without compilation, and babel is not compiling it but only preprocessing it, mainly to handle the specificities of all browsers. Based on that, I don't see what you call 'runtime' and what you call 'not runtime'. But, what I'm sure of, is that If you could give me a concrete example of what trouble you are afraid to face, it would probably make this discussion easier ;) |
Beta Was this translation helpful? Give feedback.
-
To bring this back to OP's question: how do you use For the |
Beta Was this translation helpful? Give feedback.
-
Yes, this approach requires a full app rerender to apply new styles. |
Beta Was this translation helpful? Give feedback.
-
Well, from what I remember from their code, styled-components is creating hashes for the style each component is using and then creating a stylesheet for each of those hash. So 2 components with the same style hash will share the same stylesheet. If you want to hardcode all the variations of your styles manually beforehand to avoid creating them during the execution, that seems to me like a long and unnecessary task. But probably I misunderstood what you mean. If you can explain me better I'll be happy to help. |
Beta Was this translation helpful? Give feedback.
-
In meanwhile, I've been playing with benchmarks and implemented a case with As I expected, this case has the worst performance. I created a PR, so you can try it if you want: #1839 |
Beta Was this translation helpful? Give feedback.
-
i've since changed my approach for dynamic styles.
|
Beta Was this translation helpful? Give feedback.
-
Recently I tried to implement something convenient and came up with this: https://github.com/ElForastero/pulsar-core I extended the The package is pretty unstable, but I'll improve it during the next few months as I use it in my UI library. |
Beta Was this translation helpful? Give feedback.
-
That's the approach I am using. export default function Test() {
const t = useTheme();
return (
<View>
<Text style={[t.textBase, t.mbBase, t.c, t.bc]}>text</Text>
<Text style={t.textL}>text</Text>
<Text style={t.textXL}>text</Text>
<Text style={t.textBase}>text</Text>
</View>
);
} import { useMemo } from 'react';
import { StyleSheet, useColorScheme } from 'react-native';
// Test with <link rel="stylesheet" href="//basehold.it/27"></link>
const baseFontSize = 18;
const lineHeightRatio = 1.5;
const lineHeightBase = baseFontSize * lineHeightRatio;
// http://inlehmansterms.net/2014/06/09/groove-to-a-vertical-rhythm
const fontSize = (fontSize: number) => {
const lines = Math.ceil(fontSize / lineHeightBase);
const lineHeight = lines * lineHeightBase;
return { fontSize, lineHeight };
};
export const useTheme = () => {
const colorScheme = useColorScheme() || 'light';
// RNfW will export to static styles only what has been used.
return useMemo(
() =>
StyleSheet.create({
textXS: fontSize(14),
textS: fontSize(16),
textBase: fontSize(baseFontSize),
textL: fontSize(24),
textXL: fontSize(32),
// Note vertical rhythm.
mbBase: { marginBottom: lineHeightBase },
mbL: { marginBottom: lineHeightBase * 2 },
c: {
color: colorScheme === 'light' ? '#000' : '#fff',
},
bc: {
backgroundColor: colorScheme === 'light' ? '#fff' : '#000',
},
}),
[colorScheme],
);
}; |
Beta Was this translation helpful? Give feedback.
-
This is what I came up with, the goal was to make it work with react-native-paper and custom props in a similar way that web makeStyles from Materiial UI works. the type of the theme is ReactNativePaper.Theme because I use custom theme, you can just import standard Theme type from React Native Paper :)
invoke it in a Component.styles.ts file:
and use the hook in a component:
|
Beta Was this translation helpful? Give feedback.
-
Hi,
First of all, thank you for your great work on this amazing library, Nicolas. 👏
I'm wondering which approach do you use for handling dynamic styles, like button colors, variants, responsiveness? Which one are you using for your apps in production?
Currently I have a bunch of hooks that call
StyleSheet.create()
inside.Is the following example of usage
StyleSheet.create()
inside component body really bad?I mean, there're a lot of dependencies like theme, dimensions, landscape/portrait mode and so on. We can either call
StyleSheet.create()
inside component body, or we can create a lot of StyleSheet objects ahead and then combine them.This one look like a more optimized, but we need a theme dependency here, and the only way to get that comes to my mind is having some sort or Singleton Theme class that will keep track of current theme.
What can you advice? Which approach does your team use?
UPD (27 Jan 2021):
Take a look at this library: @pulsar/core
It solves all the problems with runtime styles calculation.
Beta Was this translation helpful? Give feedback.
All reactions