From cdf1520943c7e8af8762d0cae33e88c9760e60c6 Mon Sep 17 00:00:00 2001 From: Sebastian Balay Date: Wed, 23 Feb 2022 12:54:59 +0100 Subject: [PATCH] Improve performance of useRestyle 1. Instead of running composeRestyleFunctions on every render cycle, we can do it only once when creating the restyle component. 2. Build the stylesheet based only on restyle props and not all the props received by the restyled component 3. Build map used to filter between restyle props and ordinary props only once, and not on every render cycle 4. Do not apply all restyle transformations to obtain the stylesheet, only apply the transformations that are needed based on the props that are present on the restyled component --- src/composeRestyleFunctions.ts | 33 +++++++++++------- src/createRestyleComponent.tsx | 5 ++- src/hooks/useRestyle.ts | 64 +++++++++++++++++++--------------- src/test/useRestyle.test.tsx | 19 ++++++---- 4 files changed, 72 insertions(+), 49 deletions(-) diff --git a/src/composeRestyleFunctions.ts b/src/composeRestyleFunctions.ts index c0b05ffe..6b3f817c 100644 --- a/src/composeRestyleFunctions.ts +++ b/src/composeRestyleFunctions.ts @@ -5,6 +5,7 @@ import { BaseTheme, Dimensions, RNStyle, + RestyleFunction, } from './types'; import {AllProps} from './restyleFunctions'; @@ -26,18 +27,19 @@ const composeRestyleFunctions = < const properties = flattenedRestyleFunctions.map(styleFunc => { return styleFunc.property; }); - const funcs = flattenedRestyleFunctions - .sort( - (styleFuncA, styleFuncB) => - Number(styleFuncB.variant) - Number(styleFuncA.variant), - ) - .map(styleFunc => { - return styleFunc.func; - }); + const propertiesMap = properties.reduce( + (acc, prop) => ({...acc, [prop]: true}), + {} as Record, + ); + + const funcsMap = flattenedRestyleFunctions.reduce( + (acc, each) => ({[each.property]: each.func, ...acc}), + {} as Record>, + ); // TInputProps is a superset of TProps since TProps are only the Restyle Props - const buildStyle = ( - props: TInputProps, + const buildStyle = ( + props: TProps, { theme, dimensions, @@ -46,15 +48,20 @@ const composeRestyleFunctions = < dimensions: Dimensions; }, ): RNStyle => { - const styles = funcs.reduce((acc, func) => { - return Object.assign(acc, func(props, {theme, dimensions})); - }, {}); + const styles = Object.keys(props).reduce( + (styleObj, propKey) => ({ + ...styleObj, + ...funcsMap[propKey as keyof TProps](props, {theme, dimensions}), + }), + {}, + ); const {stylesheet} = StyleSheet.create({stylesheet: styles}); return stylesheet; }; return { buildStyle, properties, + propertiesMap, }; }; diff --git a/src/createRestyleComponent.tsx b/src/createRestyleComponent.tsx index 5310ba1d..5f4daa1e 100644 --- a/src/createRestyleComponent.tsx +++ b/src/createRestyleComponent.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {View} from 'react-native'; +import composeRestyleFunctions from './composeRestyleFunctions'; import {BaseTheme, RestyleFunctionContainer} from './types'; import useRestyle from './hooks/useRestyle'; @@ -13,8 +14,10 @@ const createRestyleComponent = < | RestyleFunctionContainer[])[], BaseComponent: React.ComponentType = View, ) => { + const composedRestyleFunction = composeRestyleFunctions(restyleFunctions); + const RestyleComponent = React.forwardRef((props: Props, ref) => { - const passedProps = useRestyle(restyleFunctions, props); + const passedProps = useRestyle(composedRestyleFunction, props); return ; }); type RestyleComponentType = typeof RestyleComponent; diff --git a/src/hooks/useRestyle.ts b/src/hooks/useRestyle.ts index be476c38..75e5ef41 100644 --- a/src/hooks/useRestyle.ts +++ b/src/hooks/useRestyle.ts @@ -1,8 +1,7 @@ import {useMemo} from 'react'; -import {StyleProp} from 'react-native'; +import {StyleProp, ViewStyle, TextStyle, ImageStyle} from 'react-native'; -import {BaseTheme, RestyleFunctionContainer, RNStyle} from '../types'; -import composeRestyleFunctions from '../composeRestyleFunctions'; +import {BaseTheme, RNStyle, Dimensions} from '../types'; import {getKeys} from '../typeHelpers'; import useDimensions from './useDimensions'; @@ -13,24 +12,20 @@ const filterRestyleProps = < TProps extends Record & TRestyleProps >( props: TProps, - omitList: (keyof TRestyleProps)[], -): Omit => { - const omittedProp = omitList.reduce>( - (acc, prop) => { - acc[prop] = true; - return acc; - }, - {} as Record, - ); - + omitPropertiesMap: Record, +) => { return getKeys(props).reduce( - (acc, key) => { - if (!omittedProp[key as keyof TRestyleProps]) { - acc[key] = props[key]; + ({cleanProps, restyleProps}, key) => { + if (omitPropertiesMap[key as keyof TProps]) { + return {cleanProps, restyleProps: {...restyleProps, [key]: props[key]}}; + } else { + return {cleanProps: {...cleanProps, [key]: props[key]}, restyleProps}; } - return acc; }, - {} as TProps, + {cleanProps: {}, restyleProps: {}} as { + cleanProps: TProps; + restyleProps: TRestyleProps; + }, ); }; @@ -39,9 +34,20 @@ const useRestyle = < TRestyleProps extends Record, TProps extends TRestyleProps & {style?: StyleProp} >( - restyleFunctions: ( - | RestyleFunctionContainer - | RestyleFunctionContainer[])[], + composedRestyleFunction: { + buildStyle: ( + props: TInputProps, + { + theme, + dimensions, + }: { + theme: Theme; + dimensions: Dimensions; + }, + ) => ViewStyle | TextStyle | ImageStyle; + properties: (keyof TProps)[]; + propertiesMap: Record; + }, props: TProps, ) => { const theme = useTheme(); @@ -49,18 +55,18 @@ const useRestyle = < const dimensions = useDimensions(); const restyled = useMemo(() => { - const composedRestyleFunction = composeRestyleFunctions(restyleFunctions); - const style = composedRestyleFunction.buildStyle(props, { + const {cleanProps, restyleProps} = filterRestyleProps( + props, + composedRestyleFunction.propertiesMap, + ); + const style = composedRestyleFunction.buildStyle(restyleProps, { theme, dimensions, }); - const cleanProps = filterRestyleProps( - props, - composedRestyleFunction.properties, - ); - (cleanProps as TProps).style = [style, props.style].filter(Boolean); + + cleanProps.style = [style, props.style].filter(Boolean); return cleanProps; - }, [restyleFunctions, props, dimensions, theme]); + }, [composedRestyleFunction, props, dimensions, theme]); return restyled; }; diff --git a/src/test/useRestyle.test.tsx b/src/test/useRestyle.test.tsx index ee2e8d20..330fcffb 100644 --- a/src/test/useRestyle.test.tsx +++ b/src/test/useRestyle.test.tsx @@ -4,6 +4,7 @@ import {Text, TouchableOpacity} from 'react-native'; import useRestyle from '../hooks/useRestyle'; import {position, PositionProps} from '../restyleFunctions'; import createVariant, {VariantProps} from '../createVariant'; +import composeRestyleFunctions from '../composeRestyleFunctions'; const theme = { colors: {}, @@ -15,21 +16,27 @@ const theme = { phone: 0, tablet: 376, }, + zIndices: { + phone: 5, + }, }; type Theme = typeof theme; type Props = VariantProps & - PositionProps & { - title: string; - } & ComponentPropsWithoutRef; + PositionProps & + ComponentPropsWithoutRef; const restyleFunctions = [ position, - createVariant({themeKey: 'buttonVariants'}), + createVariant({themeKey: 'buttonVariants'}), ]; -function Button({title, ...rest}: Props) { - const props = useRestyle(restyleFunctions, rest); +const composedRestyleFunction = composeRestyleFunctions( + restyleFunctions, +); + +function Button({title, ...rest}: Props & {title: string}) { + const props = useRestyle(composedRestyleFunction, rest); return ( {title}