I've always been very aligned with the styling philosophy of breaking the elements that form a component into smaller styled pieces with the opportunity to provide meaningful semantic names for those pieces.
function MyAccount(){
return (
<Container>
<Header>
<BackButton onPress={handleGoBack}>
<ArrowLeftIcon />
</BackButton>
<HeaderText>My Account</HeaderText>
</Header>
...
</Container>
);
}
From my point of view it's easier to understand and maintain the components when the bits are named in such a way that it's clear what is the role of the component in the whole. It basically decouples what the component looks like from what it represents in the interface.
The goal is to create my own utility to achieve something similar to what styled-components
does but using objects instead of css; as well as taking advantage of Typescript's intellisense to help compose the style of a component with auto completion.
The idea is quite simple, provide a way of creating simple named styled components which could be used on smaller projects and prototypes without having to install styled-components
nor create runtime cost processing the css-ish styles.
Something like this, very straight forward:
const Heading1 = styled(Text, {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 12,
marginTop: 24,
});
Quick Note: This pattern of a function taking a component and potentially other arguments which then returns a new modified component is called Higher-order Components (HOC). The term is inspired by Higher-order Functions from functional programming which does a similar thing but with functions.
I started with a simple HOC to do the job without concerning types nor intellisense. The function should take the component and the style and return a wrapped component with the provided styles applied to it.
import React from 'react';
export function styled(
Component,
style,
) {
return (props) => {
return <Component {...props} style={[style, props.style]} />;
};
}
Basically takes a component and a style argument and return another component that bypasses the props and combine styles with the style passed. Notice that our style comes before the props.style
so that we can override styles on the high end by either providing new styles or using styled
multiple times on the resulting component.
Done! Easy peasy lemon squeezy! Looks nice and it works. Now we can start adding types to create some constraints and, after all, we wanted to take advantage of intellisense as well.
First things first, our HOC can only style actual components, therefore, we can start by enforcing this on our function to make sure no one passes in invalid components creating uncontrolled behavior. React has the perfect type for that, which is, ComponentType<P>
where P
is the properties type of that component.
-import React from 'react';
+import React, { ComponentType } from 'react';
-export function styled(
+export function styled<P>(
- Component,
+ Component: ComponentType<P>,
style,
) {
- return (props) => {
+ return (props: P) => {
return <Component {...props} style={[style, props.style]} />;
};
}
Notice the resulting component should as well be of properties type P
because the HOC is supposed to return the "same" component but styled.
Now our function only accepts components. Which is good, but we can still mess up the style argument; besides, Typescript still does nothing to help us create the style object.
We can go next by addressing this particular problem which is, the style argument must be of the same type as the component's style. This, can be done by indexing the style
property from our properties type P
, that is, P['style']
.
import React, { ComponentType } from 'react';
export function styled<P>(
Component: ComponentType<P>,
- style,
+ style?: P['style'],
) {
return (props: P) => {
return <Component {...props} style={[style, props.style]} />;
};
}
The problem now is Typescript is not happy because not all properties type P
might have style
on it. Which makes sense, right? We should only style components that can be styled, therefore, our component must have a style property.
So what we need to do to figure this constraint and consequently make typescript happy again is to enforce that the given component has a style
property in its properties. Hence, we can state that P
must have a style property on it.
"But what best defines a style property"? you might ask. Well, after some time digging the React Native type definitions I found out styles are defined by StyleProp<T>
where T
is the custom style definition of a component, that is: TextStyle
, ViewStyle
, etc; in our case, T
is not important because we already have style type info from P['style']
, hence, we can simply use unknown
and it should work just fine.
import React, { ComponentType } from 'react';
+import { StyleProp } from 'react-native';
-export function styled<P>(
+export function styled<P extends { style?: StyleProp<unknown> }>(
Component: ComponentType<P>,
style?: P['style'],
) {
return (props: P) => {
return <Component {...props} style={[style, props.style]} />;
};
}
And that's it! We are done! The function works and Typescript is happy again. Now intellisense of the style
argument suggests valid style for the given component and we are good to go.
We can even refactor the function to make it more readable, I ended up with the code bellow which is the final version of my tiny styled HOC.
import React, { ComponentType } from 'react';
import { StyleProp } from 'react-native';
type StyleableProp = { style?: StyleProp<unknown> };
export function styled<P extends StyleableProp>(
Component: ComponentType<P>,
style?: P['style'],
) {
return (props: P) => {
return <Component {...props} style={[style, props.style]} />;
};
}
Here is some usage example:
import React from 'react';
import { View, Text } from 'react-native';
import { styled } from './styled';
const Container = styled(View, {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
});
const Heading1 = styled(Text, {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 12,
marginTop: 24,
});
export default function App(){
return (
<Container>
<Heading1>Hello, world!</Heading1>
</Container>
);
}
In conclusion, I don't think by any extent this is a complete production ready solution to replace styled-components
or other similar solutions but it's a good starting point to build my own tiny styling utility suite.
Next steps would be to figure ways to change attributes/props of the component, probably using a different HOC. Also, it would be interesting to figure a good theming by context solution, like styled-components
but within the javascript/react way of doing things.
Regardless, it was a great experience exploring typescript's capabilities applied to React Native, you always end up learning really interesting stuff along the journey. Besides. it's good sometimes to build your own stuff.
I hope you've had a good read. Cheers!
Top comments (1)
Thanks for the inspiration )
You can check out this repo, which includes custom props, theming etc.
npmjs.com/package/styled-rn
It's fresh out of the oven, but works amazingly well for my own project :)