π¬ Intro
In ProntoPro, we are using React as our core library. That's why we based our architecture on react-dom
for the web application and react-native
for the iOS and Android applications.
In our Frontend codebase, we are able to differentiate implementations per platform - when needed - at any level of abstraction. Platform agnostic abstractions reference and use platform specific implementations without having to write the same code twice.
To better understand how we arrived at this solution and how we have implemented it, I highly recommend you to take a look at lucarge's talk React: write once, run everywhere.
We used this platform agnostic abstraction also to define our grid system. React Native (like any modern web browser) supports Flexbox out-of-the-box, so it provides an easy way to create a consistent layout on different screen sizes.
π Styled-components
When we started building our grid system, we decided to use styled-components, a library that provides a way to create components that have their own encapsulated styles.
We specifically chose this library because it can be used both with react
and react-native
by using the same API. This dependency is not strictly necessary, but we highly recommend it. π
π€Ήπ»ββοΈ Main components
Our grid system is based on several components that combined together can create different layouts.
View
Everything starts with the View
component:
// native/index.tsx
import styled from 'styled-components'
export const View = styled.View``
// web/index.tsx
import styled from 'styled-components'
export const View = styled.div``
We've also created a styled-components
util using the css
helper, which can be imported to add Flexbox CSS properties to every component:
import { css } from 'styled-components'
import { TGrid } from '@pp/types/grid'
export const grid = css`
display: flex;
min-height: 0;
min-width: 0;
${({
align = 'stretch',
basis = 'auto',
flexDir = 'row',
flexWrap = 'nowrap',
grow = 1,
justify = 'flex-start',
shrink = 1,
}: Partial<TGrid>) => `
align-items: ${align};
flex-basis: ${basis};
flex-direction: ${flexDir};
flex-grow: ${grow};
flex-shrink: ${shrink};
flex-wrap: ${flexWrap};
justify-content: ${justify};
`}
`
On top of our View
component, we've built our grid system by creating two other components: Column
and Row
.
The default flex-direction
is row
, but we decided to keep Column
as our core element to guarantee that there are no behavioural differences between native and web since in react-native
the default value is column
as described here. The reason behind this decision was to make the React Native View
behave like a div
(related issue).
Column
The Column
component creates a wrapper with flex-direction
defaulted to column
. This component enhances View
with specific props mapped through grid
to the respective CSS Flexbox properties:
// Column/index.tsx
export const Column = styled(View).attrs<Partial<TGrid>>(
({
align = 'stretch',
basis = 'auto',
flexDir = 'column',
flexWrap = 'nowrap',
grow = 0,
justify = 'flex-start',
shrink = 0,
}) => ({
align,
basis,
flexDir,
flexWrap,
grow,
justify,
shrink
})
)`
${grid}
`
We decided to set:
align = 'stretch'
: in case of column,align
refers to the horizontal axis, so setting it tostretch
makes its children expand horizontally.grow = 0
: think about a generic layout vertically split in two parts. By default we don't want each part to fill 50% of the space; we just want it to take the space it needs.shrink = 0
: to preserve its height and never shrink it.
Row
The Row
component creates a wrapper with flex-direction
defaulted to row
. This component enhances View
with specific props mapped through grid
to the respective CSS Flexbox properties:
// Row/index.tsx
export const Row = styled(View).attrs<Partial<TGrid>>(
({
align = 'stretch',
basis = 'auto',
flexDir = 'row',
flexWrap = 'nowrap',
grow = 1,
justify = 'flex-start',
shrink = 1,
}) => ({
align,
basis,
flexDir,
flexWrap,
grow,
justify,
shrink
})
)`
${grid}
`
We have decided to set:
align = 'stretch'
: it depends on the wrapper item (Column
orRow
), and its behaviour is to expand items so that they will all stretch to become as tall as the tallest item, as that item is defining the height of the items on the cross axis.grow = 1
: by default, we want two or more items to fill the remaining space. If wrapped inside a column, it fills the remaining vertical space; otherwise it fills the horizontal one.shrink = 1
: to make the content shrink to fit the space and not exceed the container.
SafeArea
Another important piece of the puzzle is the SafeAreaView
component, which we use in our native apps. Quoting the React Native documentation:
SafeAreaView renders nested content and automatically applies padding to reflect the portion of the view that is not covered by navigation bars, tab bars, toolbars, and other ancestor views. Moreover, and most importantly, Safe Area's paddings reflect the physical limitation of the screen, such as rounded corners or camera notches.
Since we want to have a platform-agnostic component which can be imported both in web and in native platforms, we created the SafeArea
component abstraction:
// native/index.tsx
import styled from 'styled-components'
const SafeArea = styled.SafeAreaView.attrs<Partial<TGrid>>({
flexDir: 'column',
grow: 1,
shrink: 1,
})`
${grid}
`
This component is used to wrap an entire (react-native) View
, so we have to be sure not to cause side effects. It could be stand-alone, wrap column/row, be composable (a SafeArea
inside another SafeArea
) or even have a Column
/Row
below it and the behaviour of its children should be the same.
So we decided to set:
grow = 1
: to make items fill the entire space given by its container height.shrink = 1
: to make the content not exceed the screen but shrink items to fit the space of its container.
// web/index.tsx
import styled from 'styled-components'
const SafeArea = ({ children }) => <>{children}</>
In React, it's not always possible to render an array of children; the fragment guarantees that there are no behavioral differences between react-native code (where the SafeArea
creates a single node) and react-dom code.
βοΈ Examples
You can find some usage examples in this CodeSandbox:
https://codesandbox.io/s/prontopro-grid-system-8grqp
π What's next
These are the components that define our base grid system. In the next articles you will discover how we implemented platform-agnostic abstractions to handle media queries and spacing.
That's all folks, thank you for reading and stay tuned! π