React Native Theme Provider
- React Native Theme Provider
Installation
yarn add @pavelgric/react-native-theme-provider
Usage
with initThemeProvider (Recommended)
Initialize themes and the 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'
});
Wrap your app with exported ThemeProvider
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>
);
};
Create style for 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 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>
);
};
Change or access theme
// 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>
);
};
Change or access theme params
// 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>
);
};
Usage without initThemeProvider
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>();
Wrap your app with ThemeProvider
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
Usage without any helpers
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.
Passing params to style creator
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
Passing params examples
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} />;
};
Passing and accessing default styles
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>
);
};
Passing theme params
You can pass custom params to your themes,
Exported functions
initThemeProvider
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 |
createStyle
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,
},
}));
useStyle
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} />;
};
createUseStyle
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} />;
};
useTheme
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 />;
};
useThemeDispatch
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>
);
};
createThemedStyleCreator
see Usage without initThemeProvider
createThemedUseStyleCreator
see Usage without initThemeProvider
createThemedUseTheme
see Usage without initThemeProvider
createThemedUseThemeDispatch
see Usage without initThemeProvider
createThemedUseStyle
see Usage without initThemeProvider
createThemedUseStyleWithParams
see Usage without initThemeProvider
Helper functions
createStylesWithProps
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' });
Wrappers
withUseStyle(Component, useStyleFunc, [mapPropsToParams])
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 }),
);
withCreateStyle(Component, createStyleFunc, [mapPropsToParams])
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,
);
Caching
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.
Default Cache manager
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.
Providing your own cache manager
You can pass your own cache manager to initThemeProvider
function by passing styleCacheManager
param. The styleCacheManager is an object with following props:
onProviderMount
Optional. Called when mounting the ThemeProvider component
onThemeChange
Required. Called when changing the theme in app. You should do something so the style creators re-run or correct cached values are used.
onCacheStyleCreator
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.
onCacheStyleCreator
Recommendations
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'
});
Example
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