Skip to content

Commit

Permalink
Fix variant breakpoint memoization
Browse files Browse the repository at this point in the history
  • Loading branch information
ElviraBurchik authored and Matt Frances committed Mar 23, 2023
1 parent e95eb73 commit 31ddfeb
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 55 deletions.
28 changes: 27 additions & 1 deletion src/createRestyleFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,37 @@ const createRestyleFunction = <
typeof themeKey === 'string' &&
typeof property === 'string'
) {
/*
The following code is required to ensure all variants that have different breakpoint objects are turned into unique strings. By simply retuning String(props[property]), two different variants with breakpoints will return the same string.
For example, if we have the following variant:
spacingVariant: {
defaults: {},
noPadding: {
phone: 'none',
tablet: 'none',
},
mediumPadding: {
phone: 'm',
tablet: 'm',
}
}
using String(props[property]) will turn both variants into [object Object], making them equivalent and resulting in separate styles being memoized into the same hash key.
By building the propertyValue string ourselves from the breakpoints, we can format the variants to be "phone:nonetablet:none" and "phone:mtablet:m" respectively, making each memoized hash key unique.
*/
let propertyValue = '';
if (typeof props[property] === 'object') {
for (const [breakpoint, value] of Object.entries(props[property])) {
propertyValue += `${breakpoint}:${value}`;
}
} else {
propertyValue = String(props[property]);
}

return getMemoizedMapHashKey(
dimensions,
String(themeKey),
String(property),
String(props[property]),
propertyValue,
);
} else {
return null;
Expand Down
45 changes: 45 additions & 0 deletions src/test/TestButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, {ComponentPropsWithoutRef} from 'react';
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: {},
spacing: {},
buttonVariants: {
defaults: {},
},
breakpoints: {
phone: 0,
tablet: 376,
},
zIndices: {
phone: 5,
},
};
type Theme = typeof theme;

type Props = VariantProps<Theme, 'buttonVariants'> &
PositionProps<Theme> &
ComponentPropsWithoutRef<typeof TouchableOpacity>;

const restyleFunctions = [
position,
createVariant<Theme>({themeKey: 'buttonVariants'}),
];

const composedRestyleFunction = composeRestyleFunctions<Theme, Props>(
restyleFunctions,
);

export function Button({title, ...rest}: Props & {title: string}) {
const props = useRestyle(composedRestyleFunction, rest);
return (
<TouchableOpacity {...props}>
<Text>{title}</Text>
</TouchableOpacity>
);
}
68 changes: 68 additions & 0 deletions src/test/TestContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';
import {View, ViewProps} from 'react-native';

import useRestyle from '../hooks/useRestyle';
import createVariant, {VariantProps} from '../createVariant';
import composeRestyleFunctions from '../composeRestyleFunctions';
import {
backgroundColor,
BackgroundColorProps,
BorderProps,
spacing,
SpacingProps,
} from '../restyleFunctions';

const palette = {
purple: '#5A31F4',
green: '#099C77',
black: '#101010',
white: '#FFF',
};

export const theme = {
colors: {
background: palette.purple,
},
spacing: {
none: 0,
m: 8,
},
breakpoints: {
phone: 320,
tablet: 768,
},
spacingVariant: {
defaults: {},
spacingParent: {
padding: {
phone: 'none',
tablet: 'none',
},
},
spacingNested: {
padding: {
phone: 'm',
tablet: 'm',
},
},
},
};
type Theme = typeof theme;

type RestyleProps = SpacingProps<Theme> &
BorderProps<Theme> &
BackgroundColorProps<Theme> &
VariantProps<Theme, 'spacingVariant'>;

const restyleFunctions = composeRestyleFunctions<Theme, RestyleProps>([
spacing,
backgroundColor,
createVariant({themeKey: 'spacingVariant'}),
]);

type Props = RestyleProps & ViewProps;

export const Container = ({children, ...rest}: Props) => {
const props = useRestyle(restyleFunctions, rest);
return <View {...props}>{children}</View>;
};
8 changes: 4 additions & 4 deletions src/test/createRestyleFunction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import {RNStyle} from '../types';
const theme = {
colors: {},
spacing: {},
breakpoints: {
phone: 0,
tablet: 376,
},
opacities: {
invisible: 0,
barelyVisible: 0.1,
almostOpaque: 0.9,
},
breakpoints: {
phone: 0,
tablet: 376,
},
};
const dimensions = {
width: 375,
Expand Down
107 changes: 57 additions & 50 deletions src/test/useRestyle.test.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,63 @@
import React, {ComponentPropsWithoutRef} from 'react';
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: {},
spacing: {},
buttonVariants: {
defaults: {},
},
breakpoints: {
phone: 0,
tablet: 376,
},
zIndices: {
phone: 5,
},
};
type Theme = typeof theme;

type Props = VariantProps<Theme, 'buttonVariants'> &
PositionProps<Theme> &
ComponentPropsWithoutRef<typeof TouchableOpacity>;

const restyleFunctions = [
position,
createVariant<Theme>({themeKey: 'buttonVariants'}),
];

const composedRestyleFunction = composeRestyleFunctions<Theme, Props>(
restyleFunctions,
);

function Button({title, ...rest}: Props & {title: string}) {
const props = useRestyle(composedRestyleFunction, rest);
return (
<TouchableOpacity {...props}>
<Text>{title}</Text>
</TouchableOpacity>
);
}

function Screen() {
return <Button title="test" position="absolute" />;
}
import React from 'react';
import {View} from 'react-native';
import {create as render} from 'react-test-renderer';

import {ThemeProvider} from '../context';

import {Button} from './TestButton';
import {Container, theme} from './TestContainer';

describe('Use restyle', () => {
it('creates a button', () => {
const button = Screen();
const button = <Button title="test" position="absolute" />;
expect(button.props.title).toBe('test');
});

it('uses theme props', () => {
const {root} = render(
<ThemeProvider theme={theme}>
<Container backgroundColor="background" />
</ThemeProvider>,
);

expect(root.findByType(View).props.style).toStrictEqual([
{backgroundColor: '#5A31F4'},
]);
});

it('uses theme props with variant', () => {
const {root} = render(
<ThemeProvider theme={theme}>
<Container variant="spacingParent" />
</ThemeProvider>,
);

expect(root.findByType(View).props.style).toStrictEqual([{padding: 0}]);
});

it('parent styles match theme', () => {
const {root} = render(
<ThemeProvider theme={theme}>
<Container variant="spacingParent">
<Container variant="spacingNested" />
</Container>
</ThemeProvider>,
);

expect(root.findByType(View).props.style).toStrictEqual([{padding: 0}]);
});

it('child styles match theme', () => {
const {root} = render(
<ThemeProvider theme={theme}>
<Container variant="spacingParent">
<Container variant="spacingNested" />
</Container>
</ThemeProvider>,
);

expect(root.findAllByType(View)[1].props.style).toStrictEqual([
{padding: 8},
]);
});
});

0 comments on commit 31ddfeb

Please sign in to comment.