- React Native Theme Provider
yarn add @pavelgric/react-native-theme-provider
The library provides few functions to help passing down the Theme type
Define your themes and use creator functions
// themes.ts
import {
initThemeProvider
createThemedStyleCreator,
createThemedUseStyleCreator,
createThemedUseTheme,
createThemedUseThemeDispatch,
createThemedBaseStylesCreator,
useStyle,
} from '@pavelgric/react-native-theme-provider';
const blueTheme = {
colors: {
primary: 'blue'
}
}
const redTheme = {
colors: {
primary: 'red'
}
}
// you can have as many themes as you want and their names are up to you
export const themes = {
blue: blueTheme,
red: redTheme,
};
/* create globally available styles, see further how these can be accessed */
export const baseStylesCreator = createThemedBaseStylesCreator<Themes>()((t) => ({
page: {
flex: 1,
backgroundColor: t.colors.surface,
},
flex: {
flex: 1,
},
flexCenter: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
}));
export type Themes = typeof themes;
export const {
createUseStyle,
createStyle,
useTheme,
useThemeDispatch,
ThemeProvider,
/*
* If you want to be able to use `const styles = useStyle(styleCreator, params)`, you currently need to export and use these to work with typescript correctly.
* Otherwise use the `export { useStyle };` below.
*/
// use for `const styles = useStyleThemed(styleCreator)`
useStyle: useStyleThemed,
// use for `const styles = useStyleThemedWithParams(styleCreator, params)`
useStyleWithParams: useStyleThemedWithParams,
} = initThemeProvider({ themes, initialTheme: 'red', baseStylesCreator });
// useStyle does not depend on Theme, this is just to make it also accessible from here. But you'll probably not gonna use this anyway
export { useStyle };
instead of passing themes as an object, you can also pass a function, which will receive any params you specify. This may be useful if you support changing font size or spacing based on phone size, orientation etc. These params are not propagated to style creators. If you want to pass params there, use the available solutions.
const themes = (params: { fontSizeMultiplier: number}) => {
const fontSizes = {
// other font sizes
medium: fontSizeMultiplier * 15
};
return {
light: {
// other things
fontSizes,
},
dark: {
// other things
fontSizes,
}
}
};
// will call themes as a function with initialThemeParams
const result = initThemeProvider({
themes,
initialThemeParams: {
fontSizeMultiplier: 1,
},
initialTheme: 'red'
});
import { ThemedProvider } from './path/to/themes.ts';
import { useColorScheme } from 'react-native';
export default App = () => {
const colorScheme = useColorScheme();
return (
/* You can also overwrite some values passed to initThemeProvider here, e.g. if you want to set initial theme based on systems default color scheme */
<ThemeProvider initialTheme={colorScheme === 'dark' : 'red' : 'blue'}>
<InnerComponent />
</ThemeProvider>
);
};
// InnerComponent.tsx
import React from 'react';
import { View } from 'react-native';
import { createUseStyle } from './path/to/themes.ts';
// createUseStyle basically returns (fn) => useStyle(fn)
const useStyle = createUseStyle((t) => ({
container: {
backgroundColor: t.colors.primary,
},
}));
export default InnerComponent = () => {
const styles = useStyle();
return (
/* under bs are styles created by baseStylesCreator */
<View style={styles.bs.page}>
<View style={styles.container} />
</View>
);
};
Alternatively you can use createStyle
with useStyle
combination, but it just leads to more code.
In order to receive correct types, you need to use useStyle
and useStyleWithParams
returned by initThemeProvider
.
// InnerComponent.tsx
import React from 'react';
import { View } from 'react-native';
import { createStyle, useStyleThemed, useStyleThemedWithParams } from './path/to/themes.ts';
// create styleCreator, the passed 't' is current theme object
const styleCreator = createStyle((t) => ({
container: {
backgroundColor: t.colors.primary,
},
}));
export default InnerComponent = () => {
// pass the styleCreator
const styles = useStyleThemed(styleCreator);
// or const styles = useStyleThemedWithParams(styleCreator, params);
return (
/* under bs are styles created by baseStylesCreator */
<View style={styles.bs.page}>
<View style={styles.container} />
</View>
);
};
// SomeComponent.tsx
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import {
useThemeDispatch,
useTheme,
} from './path/to/themes.ts';
export default SomeComponent = () => {
// selectedTheme is the key of selected theme
// themes is the whole themes object
// t is current theme object
const { selectedTheme, themes, t } = useTheme();
// to change theme
const { setTheme } = useThemeDispatch();
// to access current theme object. which is the same as `t` (t is the themeObj)
const themeObj = themes[selectedTheme];
return (
<View>
<Text>{`current theme is ${selectedTheme}`}</Text>
<TouchableOpacity
onPress={() => {
const nextTheme = selectedTheme === 'red' ? 'blue' : 'red';
setTheme(nextTheme);
}}
>
<Text>Change theme</Text>
</TouchableOpacity>
</View>
);
};
// SomeComponent.tsx
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import {
useThemeDispatch,
useTheme,
} from './path/to/themes.ts';
export default SomeComponent = () => {
// get current themeParams
const { themeParams } = useTheme();
// to change themeParams
const { setThemeParams } = useThemeDispatch();
return (
<View>
<Text>{`current font multiplier is ${themeParams.fontSizeMultiplier}`}</Text>
<TouchableOpacity
onPress={() => {
setThemeParams(themeParams.fontSizeMultiplier + 1);
}}
>
<Text>make font larger</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setThemeParams(themeParams.fontSizeMultiplier - 1);
}}
>
<Text>make font smaller</Text>
</TouchableOpacity>
</View>
);
};
You can theme each function individually like this, but you'll loose typing of the ThemeProvider
// themes.js
import {
initThemeProvider
createThemedStyleCreator,
createThemedUseStyleCreator,
createThemedUseTheme,
createThemedUseThemeDispatch,
useStyle,
createThemedDefaultCacheManager,
DefaultCacheManager,
createThemedUseStyle,
createThemedUseStyleWithParams,
} from '@pavelgric/react-native-theme-provider';
const blueTheme = {
colors: {
primary: 'blue'
}
}
const redTheme = {
colors: {
primary: 'red'
}
}
// you can have as many themes as you want and their names are up to you
export const themes = {
blue: blueTheme,
red: redTheme,
};
export type Themes = typeof themes;
export { useStyle };
/* create globally available styles, see further how these can be accessed */
export const baseStylesCreator = createThemedBaseStylesCreator<Themes>()((t) => ({
page: {
flex: 1,
backgroundColor: t.colors.surface,
},
flex: {
flex: 1,
},
flexCenter: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
}));
type BaseStylesCreator = ReturnType<typeof baseStylesCreator>
// caching of style creators
const cacheManager = createThemedDefaultCacheManager<Themes>(); // or create your own cacheManager
export const createStyle = createThemedStyleCreator<Themes>(cacheManager);
// alternatively export const createStyle = createThemedStyleCreator<Themes>(DefaultCacheManager)
export const createUseStyle = createThemedUseStyleCreator<Themes, BaseStylesCreator>(cacheManager);
// alternatively export const createStyle = createThemedStyleCreator<Themes>(DefaultCacheManager)
export const useTheme = createThemedUseTheme<Themes>();
export const useThemeDispatch = createThemedUseThemeDispatch<Themes>();
// If you want to be able to use `const styles = useStyle(styleCreator, params)`, you currently need to export and use these to work with typescript correctly.
// use for `const styles = useStyle(styleCreator)`
export const useStyle = createThemedUseStyle<Themes, BaseStylesCreator>();
// use for `const styles = useStyleWithParams(styleCreator, params)`
export const useStyleWithParams = createThemedUseStyleWithParams<Themes, BaseStylesCreator>();
import { ThemedProvider } from '@pavelgric/react-native-theme-provider';
import { useColorScheme } from 'react-native';
export default App = () => {
const colorScheme = useColorScheme();
return (
<ThemeProvider themes={themes} initialTheme={colorScheme === 'dark' : 'red' : 'blue'}>
<InnerComponent />
</ThemeProvider>
);
};
Otherwise the usage is the same as with using the initThemeProvider
You import functions directly from @pavelgric/react-native-theme-provider
(createStyle
, useStyle
, createUseStyle
and others). There is no style default caching using library this way. Otherwise the usage of functions is same as above.
You can pass params to useStyle
and similar functions, which will be accessible in createStyle
and similar.
When using typescript, you will be alerted if you specify params in createStyle
or createUseStyle
, but do not pass them into useStyle
.
⚠️ Be aware that if you pass params this way, the the style creator is not cached and re-runs for every single component. But you can create and pass your own cache manager to handle this case, see here for more information Caching
using createStyle
and useStyle
combination
// InnerComponent.js
import React from 'react';
import { View } from 'react-native';
import { createStyle, useStyle } from '@pavelgric/react-native-theme-provider';
const styleCreator = createStyle(
(t, { borderColor }: { borderColor: string }) => ({
container: {
backgroundColor: t.colors.primary,
borderColor,
},
}),
);
export default InnerComponent = () => {
const styles = useStyle(styleCreator, { borderColor: 'blue' });
return <View style={styles.container} />;
};
using createUseStyle
// InnerComponent.js
import React from 'react';
import { View } from 'react-native';
import { createUseStyle } from '@pavelgric/react-native-theme-provider';
const useStyle = createUseStyle(
(t, { borderColor }: { borderColor: string }) => ({
container: {
backgroundColor: t.colors.primary,
borderColor,
},
}),
);
export default InnerComponent = () => {
const styles = useStyle({ borderColor: 'blue' });
return <View style={styles.container} />;
};
You can create and access some basic styles like { flex: 1 }
, so you don't have to recreate them everywhere over and over again.
These styles will be passed to you by the const styles = useStyle()
function and will be (by default) under styles.bs
key. The bs
key is reserved and you can't overwrite it in your style creator, meaning you can't have there some bs: { flex: 1}
. You can change the bs
key to whatever you like with baseStylesKey
prop.
This applies only if you pass the baseStylesCreator
to the initThemeProvider
or ThemeProvider
, it's not required.
The bs
object is singleton and is the same in all styles
objects from useStyle
.
First create base styles
import { createThemedBaseStylesCreator } from '@pavelgric/react-native-theme-provider';
export const themes = {
// ...some themes
}
export const baseStylesCreator = createThemedBaseStylesCreator<typeof themes>()((t) => ({
page: {
flex: 1,
backgroundColor: t.colors.surface,
},
flex: {
flex: 1,
},
flexCenter: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
}));
Pass them either to initThemeProvider
or ThemeProvider
directly
export const {
createUseStyle,
createStyle,
useTheme,
useThemeDispatch,
ThemeProvider,
} = initThemeProvider({
themes,
initialTheme: 'red',
baseStylesCreator,
baseStylesKey: 'customBS' // base styles will be then accessible in styles.customBS
});
// OR
export default App = () => {
const colorScheme = useColorScheme();
return (
/* You can also overwrite some values passed to initThemeProvider here, e.g. if you want to set initial theme based on systems default color scheme */
<ThemeProvider
baseStylesCreator
initialTheme={colorScheme === 'dark' : 'red' : 'blue'}
baseStylesKey="customBS"
>
<InnerComponent />
</ThemeProvider>
);
};
Access them in your component
// InnerComponent.tsx
import React from 'react';
import { View } from 'react-native';
import { createUseStyle } from './path/to/themes.ts';
// createUseStyle basically returns (fn) => useStyle(fn)
const useStyle = createUseStyle((t) => ({
container: {
backgroundColor: t.colors.primary,
},
}));
export default InnerComponent = () => {
const styles = useStyle();
return (
/* under `customBS` (`bs` by default) key are styles created by baseStylesCreator. */
<View style={styles.customBS.page}>
<View style={styles.container} />
</View>
);
};
You can pass custom params to your themes,
Helper function to generate all needed functions and type them with Typescript
Styles creators created by this function are cached, see Caching.
import {
initThemeProvider,
DefaultCacheManager
} from '@pavelgric/react-native-theme-provider';
const blueTheme = {
colors: {
primary: 'blue'
}
}
const redTheme = {
colors: {
primary: 'red'
}
}
// you can have as many themes as you want
export const themes = {
blue: blueTheme,
red: redTheme,
};
export const {
createUseStyle,
createStyle,
useTheme,
useThemeDispatch,
ThemeProvider,
} = initThemeProvider({
themes,
initialTheme: 'light',
onThemeChange: (nextThemeName) => {}
styleCreatorCache: DefaultCacheManager
// see table below for other props
});
Param | Type | Required | description |
---|---|---|---|
themes | Object | Yes | Themes object containing app themes |
initialTheme | String | Yes | Name of one of the theme |
onThemeChange | Function | No | Called when theme changes |
initialThemeParams | Object | Yes | Initial theme params |
themeKey | String | Yes | By default values of selected theme returned by useTheme are accessed via t keyword, use this prop to change it to other key. E.g. by setting it to "customT" you will access theme like this const { customT } = useTheme() . Can be useful to avoid name collisions, as t is often used with translations |
onThemeParamsChange | Function | No | Called when theme params change |
styleCacheManager | Object | No | Object with functions to handle style caching. By default uses DefaultCacheManager |
baseStylesCreator | Function | No | Used to create base styles, which can be accessed in every styles object returned from useStyles |
baseStylesKey | String | No | Used with baseStylesCreator . By default the styles are accessible via bs keyword, use this prop to change it to other key. E.g. by setting it to "customBS" you will access base styles in styles.customBS |
This function is to create style object, similar to StyleSheet.create
, except you receive the theme object and optional params. The function returns styleCreator, which is to be passed to useStyle
.
import { createStyle } from '@pavelgric/react-native-theme-provider';
const styleCreator = createStyle((t, passedParams) => ({
container: {
backgroundColor: t.colors.primary,
opacity: passedParams.disabled ? 0.5 : 1,
},
}));
This function accepts styleCreator created with createStyle
, and returns style object returned by styleCreator.
You can also pass second argument, which can be then accessed in styleCreator.
⚠️ Be aware that if you pass params this way, the the style creator is not cached and re-runs for every single component. But you can create and pass your own cache manager to handle this case, see here for more information Caching
import { useMemo } from 'react';
import { useStyle } from '@pavelgric/react-native-theme-provider';
import styleCreator from './styles';
export default FooComponent = ({ disabled }) => {
// memoize params, to prevent unnecessary renders
const styleParams = useMemo(() => ({ disabled }), [disabled]);
const styles = useStyle(styleCreator, styleParams);
return <YourComponents style={styles.container} />;
};
This combines createStyle
and useStyle
into one function and returns useStyle
function for direct use.
import { createUseStyle } from '@pavelgric/react-native-theme-provider';
const useStyle = createUseStyle((t, passedParams) => ({
container: {
backgroundColor: t.colors.primary,
opacity: passedParams.disabled ? 0.5 : 1,
},
}));
export default FooComponent = ({ disabled }) => {
// memoize params, to prevent unnecessary renders
const styleParams = useMemo(() => ({ disabled }), [disabled]);
const styles = useStyle(styleParams);
return <YourComponents style={styles.container} />;
};
Access theme in any Component.
import { useTheme } from '@pavelgric/react-native-theme-provider';
export default SomeComponent = () => {
const {
selectedTheme, // the key of selected theme
themes, // the whole themes object
t, // current theme object. This key can be changed by setting `themeKey` in ThemeProvider
} = useTheme();
const { setTheme } = useThemeDispatch();
// to access current theme object, or use t
const themeObj = themes[selectedTheme];
return <Component />;
};
Change theme
import { useThemeDispatch } from '@pavelgric/react-native-theme-provider';
export default SomeComponent = () => {
const { setTheme } = useThemeDispatch();
return (
<View>
<TouchableOpacity
onPress={() => {
setTheme('blue');
}}
>
<Text>Set blue theme</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setTheme('red');
}}
>
<Text>Set red theme</Text>
</TouchableOpacity>
</View>
);
};
see Usage without initThemeProvider
see Usage without initThemeProvider
see Usage without initThemeProvider
see Usage without initThemeProvider
see Usage without initThemeProvider
see Usage without initThemeProvider
This method allows you to create styles similarly to StyleSheet.create
, but allows you to pass props.
This method is used to create base styles in example project.
import { createStylesWithProps } from '@pavelgric/react-native-theme-provider';
type Colors = {
primary: string,
secondary: string,
};
// first create the style creator
const createStyles = createStylesWithProps((colors: Colors) => ({
page: {
backgroundColor: colors.surface,
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
}));
// now we can use this function and pass props
const lightStyles = createStyles({ primary: 'blue', secondary: 'light-blue' });
const darkStyles = createStyles({ primary: 'dark-blue', secondary: 'blue' });
Passes styles as prop to class component created from useStyle
.
import { useStyle, withUseStyle } from '@pavelgric/react-native-theme-provider';
const useStyle = createUseStyle((t, passedParams) => ({
container: {
backgroundColor: t.colors.primary,
opacity: passedParams.disabled ? 0.5 : 1,
},
}));
type ClassCompProps = {
styles: ReturnType<typeof useStyle>,
disabled: boolean,
};
export class ClassComp extends Component<ClassCompProps> {
render() {
const { styles } = this.props;
return <View style={styles.container} />;
}
}
const ClassCompWithStyle = withUseStyle(
ClassComp,
useStyle,
({ disabled }) => ({ disabled }),
);
Passes styles as prop to class component created from createStyle using styleCreator
.
import {
styleCreator,
withCreateStyle,
} from '@pavelgric/react-native-theme-provider';
const styleCreator = createStyle((t, passedParams) => ({
container: {
backgroundColor: t.colors.primary,
opacity: passedParams.disabled ? 0.5 : 1,
},
}));
type ClassCompProps = {
styles: ReturnType<typeof styleCreator>,
disabled: boolean,
};
export class ClassComp extends Component<ClassCompProps> {
render() {
const { styles } = this.props;
return <View style={styles.container} />;
}
}
const ClassCompWithStyle = withCreateStyle(
ClassComp,
styleCreator,
({ disabled }) => ({ disabled }),
);
const ClassCompWithStyleMemoized = withCreateStyle(
ClassComp,
styleCreator,
undefined,
);
imagine Image you have ThemedText
component used all over the place, without caching, each useStyle
would create new style object, but we can cache the style object, so it is created only once and then just reused.
By using the initThemeProvider
style creators are cached. To take advantage of the cache, you currently can't pass params to useStyle
function. If you pass params, then the style creator is re-run every time for each component.
You can pass your own cache manager to initThemeProvider
function by passing styleCacheManager
param. The styleCacheManager is an object with following props:
Optional. Called when mounting the ThemeProvider component
Required. Called when changing the theme in app. You should do something so the style creators re-run or correct cached values are used.
Required. This is the function run for every style creator - function inside createStyle or createUseStyle. It receives the style creator and you can cache the resulting style or return already cached style.
Use creator function to ensure all theme objects have same shape, otherwise keys that are not present in all theme objects will be excluded by Typescript.
// color pallete
const pallete = {
red: 'red',
blue: 'blue',
};
type Color = 'primary';
type Props = {
colors: Record<Color, string>,
};
export const createTheme = (props: Props) => ({
colors: props.colors,
});
const redThemeColors = {
primary: pallete.red,
};
const blueThemeColors = {
primary: pallete.blue,
};
export const redTheme = createTheme({ colors: redThemeColors });
export const blueTheme = createTheme({ colors: blueThemeColors });
export const themes = {
redTheme,
blueTheme,
};
It would be nice to have function that would warn about missing keys, but I didn't find way how to do that. Ideally something like:
const blueTheme = {
colors: {
primary: 'blue',
secondary: 'yellow',
},
};
const redTheme = {
colors: {
primary: 'red',
},
};
export const themes = createThemes({
blueTheme,
redTheme, // warn red theme is missing key 'secondary'
});
See example for semi-complete solution
to run example, you need to install dependencies both on package and example level, so navigate to this package in terminal and run
yarn && cd example && yarn && cd ios && pod install
;
To modify the lib code and see changes immediately, do following changes in ./package.json
"main": "src/index.ts", // <-- change to this
"types": "src/index.ts", // <-- change to this