Box layout component
PUBLISHED ON: Thursday, Aug 11, 2022
#
Motivation for layout components
Layout components are for sections of your app that you want to share across multiple screens.
Each of these layout components can consist of one atomic component which we will call the Box
component.
Let's try to create a Box
component which will be at the core of our layout components.
Our end result will be as follows:
<Box
items="center"
justify="center"
m="l"
w={100}
h={100}
bg="BT.6"
border={10}
borderColor="G.2"
borderStyle="dashed"
rounded={6}
elevation={10}
/>
#
Recipe for <Box />
We will need three basic things to create a Box
component:
- It should be able to forward a reference through a component to it's root component (the wrapper
View
in our case) - Props should be easy to use and understand
- Respects the defaults for app to avoid confusion and better familiarisation among developers (😄 this implementation can be easily scaled to web) - for example, default direction is column for a
View
in React Native whereas it's row for web.
This is how the implementation looks like:
export const Box = React.forwardRef(
({ children, ...props }: ContainerProps, ref) => {
const { style: boxStyles, rest } = useBoxStyles(props);
return (
<View style={boxStyles} ref={ref as any} {...rest}>
{children}
</View>
);
},
);
The useBoxStyles
hook plays a pivotal role here. This hook will compute and return styles for the Box
component using custom props that we define with type ContainerProps
.
#
The ComponentProps type
I took some inspiration from TailwindCSS while deciding on prop names.
The idea behind prop names is to save on development time and keep things clean and easy to understand.
import { ReactFragment, ReactNode, ReactPortal } from 'react';
import {
ImageStyle,
TextStyle,
ViewStyle,
FlexStyle,
ViewProps,
Animated,
} from 'react-native';
export type AnimatedProps<T> = {
[key in keyof T]: Animated.WithAnimatedValue<T[key]>;
};
export type AnimatedStyle = AnimatedProps<ViewStyle>;
export type AnimatedValueType<T> =
| T
| Animated.Value
| Animated.AnimatedInterpolation;
export type RNStyle =
| ViewStyle
| TextStyle
| ImageStyle
| FlexStyle
| AnimatedStyle;
export enum JUSTIFY_CONTENT {
start = 'flex-start',
end = 'flex-end',
center = 'center',
between = 'space-between',
around = 'space-around',
evenly = 'space-evenly',
}
export enum ALIGN_CONTENT {
start = 'flex-start',
end = 'flex-end',
center = 'center',
between = 'space-between',
around = 'space-around',
stretch = 'stretch',
}
export enum ALIGN_ITEMS {
start = 'flex-start',
end = 'flex-end',
center = 'center',
baseline = 'baseline',
stretch = 'stretch',
}
export enum ALIGN_SELF {
start = 'flex-start',
end = 'flex-end',
center = 'center',
baseline = 'baseline',
stretch = 'stretch',
auto = 'auto',
}
type MarginKeys = 'mx' | 'my' | 'mb' | 'mr' | 'ml' | 'mt' | 'm';
type PaddingKeys = 'px' | 'py' | 'pb' | 'pr' | 'pl' | 'pt' | 'p';
type BorderColorKeys =
| 'borderColor'
| 'borderColorT'
| 'borderColorR'
| 'borderColorB'
| 'borderColorL'
| 'borderColorX'
| 'borderColorY';
type BorderKeys =
| 'border'
| 'borderT'
| 'borderR'
| 'borderB'
| 'borderL'
| 'borderX'
| 'borderY';
type RoundedKeys =
| 'rounded'
| 'roundedTL'
| 'roundedTR'
| 'roundedBL'
| 'roundedBR';
type DimensionKeys = 'w' | 'h' | 'minW' | 'minH' | 'maxW' | 'maxH';
type LayoutKeys = 'top' | 'left' | 'right' | 'bottom';
type MarginType = Record<MarginKeys, number>;
type PaddingType = Record<PaddingKeys, number>;
type BorderColorType = Record<BorderColorKeys, string>;
type BorderWidthType = Record<BorderKeys, number>;
type BorderRadiusType = Record<RoundedKeys, number>;
type DimensionType = Record<DimensionKeys, string | number>;
type BorderStyleType = 'solid' | 'dotted' | 'dashed';
type LayoutType = Record<LayoutKeys, number>;
type FlexStyleType = {
/**
* Short for justifyContent
*
* justifyContent describes how to align children within the main axis of their container.
*
* By default, it is set to "start"
*/
justify: keyof typeof JUSTIFY_CONTENT;
/**
* Short for alignContent
*
* alignContent defines the distribution of lines along the cross-axis.
*
* By default, it is set to "start"
*/
content: keyof typeof ALIGN_CONTENT;
/**
* Short for alignItems
*
* alignItems describes how to align children along the cross axis of their container.
*
* By default, it is set to "stretch"
*/
items: keyof typeof ALIGN_ITEMS;
/**
* Short for alignSelf
*
* alignSelf has the same options and effect as alignItems but
* instead of affecting the children within a container,
* you can apply this property to a single child to change its alignment within its parent.
*
* By default, it is set to "stretch"
*/
self: keyof typeof ALIGN_SELF;
/**
* Short for flexWrap
*
* The flexWrap property is set on containers and it controls
* what happens when children overflow the size of the container along the main axis.
*
* By default, it is false - children are forced into a single line
*/
wrap: boolean;
/**
* Short for flexDirection
*
* flexDirection controls the direction in which the children of a node are laid out.
*
* By default, it is column
*/
direction?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
/**
* flex will define how your items are going to “fill”
* over the available space along your main axis.
* Space will be divided according to each element's flex property.
*
* By default, it is 0
*/
flex?: number;
/**
* Short for flexGrow
*
* flexGrow describes how any space within a container should be
* distributed among its children along the main axis.
* After laying out its children, a container will distribute
* any remaining space according to the flex grow values specified by its children.
*
* flexGrow accepts any floating point value >= 0.
*
* By default, it is 0
*/
grow?: number;
/**
* Short for flexShrink
*
* flexShrink describes how to shrink children along the main axis
* in the case in which the total size of the children overflows
* the size of the container on the main axis.
* flexShrink is very similar to flexGrow and can be thought of
* in the same way if any overflowing size is considered to be negative remaining space.
*
* By default, it is 0
*/
shrink?: number;
/**
* Short for flexBasis
*
* flexBasis is an axis-independent way of providing
* the default size of an item along the main axis.
* Setting the flexBasis of a child is similar to setting
* the width of that child if its parent is a container
* with flexDirection: row or
* setting the height of a child if its parent is a container
* with flexDirection: column.
* The flexBasis of an item is the default size of that item,
* the size of the item before any flexGrow and flexShrink calculations are performed.
*
* By default, it is set to 'auto'
*/
basis?: number | 'auto';
};
export type ContainerProps = Partial<MarginType> &
Partial<PaddingType> &
Partial<BorderColorType> &
Partial<BorderWidthType> &
Partial<DimensionType> &
Partial<BorderRadiusType> &
Partial<FlexStyleType> &
Partial<LayoutType> &
ViewProps & {
/**
* By default, position is relative
*/
position?: 'absolute' | 'relative';
children?: ReactFragment | ReactPortal | ReactNode;
/**
* Determines box opacity
*/
opacity?: number;
/**
* Determines if box visibility
*/
visible?: boolean;
/**
* Background color - can be either one of the defined color strings or any hex color code
*/
bg?: string;
/**
* zIndex controls which components display on top of others
*
* By default, it is 0
*/
z?: number;
/**
* borderStyle - Has the following values:
* 1. solid
* 2. dotted
* 3. dashed
*/
borderStyle?: BorderStyleType;
/**
* Provides shadow effect
*/
elevation?: number;
/**
* Provide custom styles as an array of style objects
*/
style?: RNStyle[] | RNStyle;
};
#
The useBoxStyles() hook
useBoxStyles
is responsible for creating the final styles object that is passed as prop to the View
in Box
.
This hook respects and takes care of the use cases and combinations related to props based on priority, such as, if both marginVertical
and marginTop
are provided, then marginVertical
will take higher priority.
import { ViewProps } from 'react-native';
import { createUseStyles } from "./hooks/createUseStyles";
import {
ALIGN_CONTENT,
ALIGN_ITEMS,
ALIGN_SELF,
JUSTIFY_CONTENT,
ContainerProps,
RNStyle,
} from './types';
const getBoxStyles = ({
m = 0,
mx = m,
my = m,
mt = my,
mb = my,
mr = mx,
ml = mx,
p = 0,
px = p,
py = p,
pt = py,
pb = py,
pr = px,
pl = px,
border = 0,
borderX = border,
borderY = border,
borderT = borderY,
borderB = borderY,
borderR = borderX,
borderL = borderX,
borderColor = 'transparent',
borderColorX = borderColor,
borderColorY = borderColor,
borderColorT = borderColorY,
borderColorB = borderColorY,
borderColorL = borderColorX,
borderColorR = borderColorX,
rounded = 0,
roundedTL = rounded,
roundedTR = rounded,
roundedBL = rounded,
roundedBR = rounded,
borderStyle = 'solid',
elevation = 0,
visible = true,
opacity = 1,
bg = 'transparent',
w = 'auto',
h = 'auto',
minW = 'auto',
maxW = 'auto',
minH = 'auto',
maxH = 'auto',
position = 'relative',
top = 0,
left = 0,
right = 0,
bottom = 0,
style = [],
direction = 'column',
flex = 0,
basis = 'auto',
grow = 0,
shrink = 0,
justify = 'start',
items = 'stretch',
content = 'start',
self = 'auto',
wrap = false,
z = 0,
...rest
}: ContainerProps): { style: RNStyle[]; rest: ViewProps } => {
const marginStyle = {
marginTop: mt,
marginRight: mr,
marginBottom: mb,
marginLeft: ml,
};
const paddingStyle = {
paddingTop: pt,
paddingRight: pr,
paddingBottom: pb,
paddingLeft: pl,
};
const borderWidthStyle = {
borderTopWidth: borderT,
borderLeftWidth: borderL,
borderRightWidth: borderR,
borderBottomWidth: borderB,
};
const borderColorStyle = {
borderTopColor: borderColorT,
borderLeftColor: borderColorL,
borderRightColor: borderColorR,
borderBottomColor: borderColorB,
};
const borderRadiusStyle = {
borderTopLeftRadius: roundedTL,
borderTopRightRadius: roundedTR,
borderBottomLeftRadius: roundedBL,
borderBottomRightRadius: roundedBR,
};
const dimensions = {
minWidth: minW,
minHeight: minH,
maxWidth: maxW,
maxHeight: maxH,
width: w,
height: h,
};
const flexStyles = {
flexDirection: direction,
flex,
flexBasis: basis,
flexGrow: grow,
flexShrink: shrink,
justifyContent: JUSTIFY_CONTENT[justify],
alignItems: ALIGN_ITEMS[items],
alignSelf: ALIGN_SELF[self],
alignContent: ALIGN_CONTENT[content],
};
return {
style: [
{ display: visible ? 'flex' : 'none' },
marginStyle,
paddingStyle,
{ borderStyle },
borderWidthStyle,
borderColorStyle,
borderRadiusStyle,
flexStyles,
dimensions,
{
flexWrap: wrap ? 'wrap' : 'nowrap',
elevation,
opacity,
backgroundColor: bg,
position,
top,
left,
right,
bottom,
zIndex: z,
},
style,
],
rest,
};
};
export const useBoxStyles = createUseStyles(getBoxStyles);
Notice, we are returning two things from this function:
styles
- an array of style objects that will be applied to theView
rest
- the rest of the props that will be passed to theView
We also use a createUseStyles
function to create the useBoxStyles
hook. You can read more about it 🔗 here.
And there we have it, our 💖 Box
📦 component.
#
References