Write your own lightweight grid system for React and React Native
January 08, 2020 Β  Β· Β 3min
Author:Β PaoloΒ Rovella

🎬 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 to stretch 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 or Row), 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! πŸ‘‹

Thank you