Andrei Calazans

A GridView for React Native

☕️ 3 min read

A GridView is a component that can render a list of items and group the items by the crossAxisCount.

When you try to render 7 items with a crossAxisCount of two it renders a grid of 2 x 4.

While this seems simple, I noticed many implement this UI element by using percentage values or absolute values based on the Dimension’s width and height. You could do it with a FlatList and the numColumns prop, however if you don’t want the trailling item to grow the entire width at the bottom you would need to calculate the width of the last item for it to work or add a ghost item in your data array to have an even list of 8 for the grid of 2 x 4 to work.

Not to mention that with a FlatList the ItemSeparatorComponent only adds a separator/spacer in between the main axis elements, leaving you to implement the cross-axis spacing yourself.

Possible example with FlatList:

const App = () => {
  return (
    <SafeAreaView>
      <StatusBar />
      <View style={paddingTen}>
        <FlatList
          numColumns={2}
          data={Array.from({length: 7}).map((_, idx) => idx + 1)}
          ItemSeparatorComponent={() => <Spacer spacing={10} />}
          renderItem={({item}) => (
            <View style={{flex: 1}}> // Last item would need to calculate the absolute width
              <Choice>{item}</Choice>
            </View>
          )}
        />
      </View>
    </SafeAreaView>
  );
};

Flex Box & ScrollView

You can have a better abstraction by using Flex Box and a ScrollView.

The below demo is a display of how you can implement a Grid like component by using pure flex box and also giving control of the width and height to each rendered item. This would in thesis allow you to create dynamic grids.

API: GridView

crossAxisCount - number of items per row. mainAxisSpacing - spacing between the rows. crossAxisSpacing - spacing between the columns.

With different crossAxisCount you can achieve different outcomes

import React from 'react';
import {
  SafeAreaView,
  StatusBar,
  ScrollView,
  View,
  Text,
  StyleSheet,
} from 'react-native';

const Spacer = ({spacing, axis = 'Vertical'}) => (
  <View style={axis === 'Vertical' ? {height: spacing} : {width: spacing}} />
);

const intersperse = (item, array) =>
  array.reduce((acc, each, index) => {
    const isLast = index + 1 === array.length;
    if (isLast) {
      return [...acc, each];
    }
    return [...acc, each, item];
  }, []);

const flexOne = {flex: 1};
const backgroundBlue = {backgroundColor: 'blue'};

const GridView = ({
  children,
  crossAxisCount,
  mainAxisSpacing = 0,
  crossAxisSpacing = 0,
}) => {
  const defaultSection = Array.from({length: crossAxisCount}).map(() => null);
  const numberOfColumns = Math.round(children.length / crossAxisCount);
  let copiedChildren = children.slice();

  const flexRow = {
    flexDirection: 'row',
  };

  const getItemsForRow = () =>
    defaultSection.map(() =>
      copiedChildren.length ? (
        <View style={flexOne}>{copiedChildren.shift()}</View>
      ) : (
        <View style={flexOne} />
      ),
    );

  const list = Array.from({length: numberOfColumns}).map((_, idx) => (
    <View style={flexRow} key={idx}>
      {intersperse(
        <Spacer axis="Horizontal" spacing={crossAxisSpacing} />,
        getItemsForRow(),
      )}
    </View>
  ));

  return (
    <ScrollView>
      {intersperse(<Spacer spacing={mainAxisSpacing} />, list)}
    </ScrollView>
  );
};

const paddingTen = {padding: 10};

const Choice = ({children}) => (
  <View style={[paddingTen, backgroundBlue]}>
    <Text>{children}</Text>
    <Text>Description</Text>
  </View>
);

const App = () => {
  return (
    <SafeAreaView>
      <StatusBar />
      <View style={paddingTen}>
        <GridView crossAxisCount={3} mainAxisSpacing={10} crossAxisSpacing={10}>
          <Choice>One</Choice>
          <Choice>Two</Choice>
          <Choice>Three</Choice>
          <Choice>Four</Choice>
          <Choice>Five</Choice>
          <Choice>Six</Choice>
          <Choice>Seven</Choice>
        </GridView>
      </View>
    </SafeAreaView>
  );
};

export default App;