React Native - Dynamic Flatlist

React Native - Dynamic Flatlist

A way of calculating the numColumns property of a React Native Flatlist to make it dynamic and adaptable to changing screen sizes and orientations

Featured on Hashnode

Introduction

Hi, I'm Fabian and this is my first blog post. I recently started learning about React Native. The other day, I was looking for a way to dynamically adjust the columns of a React Native Flatlist based on the screen size and orientation (horizontal or vertical) of the device. By the end of this article, you'll be able to create such a Flatlist:

output-onlinegiftools (3).gif

Basics

First, let's start with a simple dataset for the data property of our Flatlist. For this example, let's take a data array that contains objects that each have just a key with an increasing number.

Now, we can already represent this data with a few lines of code. In addition to the data property of the Flatlist, we also need the renderItem prop, which in this case simply makes use of the keys of our array that we just created.

import { FlatList } from 'react-native';

const data = [
  { key: '1' }, { key: '2' }, { key: '3' }, { key: '4' }, { key: '5' }, 
  { key: '6' }, { key: '7' }, { key: '8' }, { key: '9' }
];

const App = () => {
    return (
      <FlatList
        data={data}
        renderItem={({item}) => { return item.key}}
      />
    );
}

export default App;

This list is still a long way from our desired result, but it already represents our desired data. If we now want to display this list in columns, we can use the numColumns property. For this, we define a constant with a value of 3 (we will change that in a moment).

At the same time, let's define a style that will center the objects, give them a background, and set them to a fixed height and width to form rectangles. In order for us to be able to see these, we also set a margin to 1. Furthermore, let's define a flex of 1 on a view enclosing the text to ensure automatic adjustment of the width of the rectangles.

const numColumns = 3;

const App = () => {
    return (
      <FlatList
        data={data}
        numColumns={numColumns}
        renderItem={({item}) => {
          return (
          <View style={styles.item}>
            <Text style={styles.itemText}>{item.key}</Text>
          </View>)
        }}
      />
    );
}


const styles = StyleSheet.create({
  item: {
    backgroundColor: '#A1A1A1',
    alignItems: 'center',
    justifyContent: 'center',
    flex: 1,
    margin: 1,
    height: 120,
    width: 90
  },
  itemText: {
    color: '#fff',
  },
});

Empty items

Since we currently have nine elements, which we represent in three columns, we have three rows with three elements each. This makes everything look evenly distributed. However, if we now remove an element, we see that this is no longer the case.

image.png

Due to the added flex of 1, React Native adjusts the width of each object of each row to fill the entire width of the screen (except the margin). If this is your desired result, you can skip to the next header. If not, let's define a new function called formatData.

This function takes two parameters, data and numColumns. Given these parameters, we can calculate how many empty objects we need to add to our existing data array to fill the last row. If, as in this case, we have two items in the last row, we want to add an empty object to that row to reflect the column count of 3.

const formatData = (data, numColumns) => {
  const amountFullRows = Math.floor(data.length / numColumns);
  let amountItemsLastRow = data.length - amountFullRows * numColumns;

  while (amountItemsLastRow !== numColumns && amountItemsLastRow !== 0) {
    data.push({key: `empty-${amountItemsLastRow}`, empty: true});
    amountItemsLastRow++;
  }
  return data;
};

This method first calculates the number of full rows and subtracts that multiplied by the number of columns from our total number of objects in the data array to calculate the number of objects left over.

Practically speaking, it looks at how many elements have already been served and still have to be served. This number can be either 0 if all have already been resolved, or less than the number of columns. Therefore, in the last step, new objects are added to the data array until the number of elements in the last row corresponds to the required number of columns (amountItemsLastRow === numColumns).

It is important to note that the added objects must also receive a unique key. The boolean empty is used in the renderItem prop to display an empty object as such. To keep things simple, we're also relocating the renderItem as a standalone method.

const renderItem = ({ item, index }) => {
    if (item.empty) {
      return <View style={[styles.item, styles.itemTransparent]} />;
    }
    return (
      <View style={styles.item}>
        <Text style={styles.itemText}>{item.key}</Text>
      </View>
    );
  };

const App = () => {
    return (
      <FlatList
        ...
        renderItem={renderItem}
      />
    );
}

As you may have noticed, there is now an additional style that is applied in the renderItem function if the object has the boolean empty, and it is true to make the background transparent.

Dynamic numColumns

Now let's get to the part we've been waiting for: dynamically adjusting the number of columns. In order to achieve this, we need to keep in mind what can happen and how we want our app to react. The user can rotate their phone so that the view can switch from vertical to horizontal mode or vice versa. Another example is Samsung's foldables, which can change their view by opening or closing the device.

Since this also changes the width and height of the window each time, we track the width using the useWindowsDimesions hook for this application. This allows the width to be destructured at the beginning of our component.

const {width} = useWindowDimensions();

By using the React useState we can now replace the numColumns constant accordingly and set the initial state to a function that we will implement later in order to be able to calculate the number of columns depending on the width that has just been destructured.

const [numColumns, setNumColumns] = useState(calcNumColumns(width));

The useEffect hook helps us detect a change in the width variable through useWindowDimensions, and then replace the numColumns with a recalculation using the updated width.

useEffect(() => {
    setNumColumns(calcNumColumns(width));
  }, [width]);

The only thing left now is to write our function for calculating the columns. First, we have to think about which calculation we want to use to determine the number. We calculate the not rounded count cols by dividing the width passed in by the width of one of our objects .

const calcNumColumns = (width) => {
  const cols = width / styles.item.width;
};

We also specify a constant colsFloor, which either takes on the value of a fixed minimum number of numColumns, or, if our just calculated cols is larger when rounded down, should equal this value.

const minCols = 2;

const calcNumColumns = (width) => {
  const cols = width / styles.item.width;
  const colsFloor = Math.floor(cols) > minCols ? Math.floor(cols) : minCols;
};

From this, we now have to subtract the total margin to calculate how many columns fit in one row. If we temporarily increase the margin to 15, for example, we see that the spacing at the edges of the screen is half the spacing between objects.

Untitled.png

This is because the margin applies to each object and is therefore duplicated between the objects themselves. We take this into account in our calculation by multiplying the rounded number of columns by the generated distance twice. This allows us to derive the total margin and subtract that from cols to know if our colsFloor fit into cols along with that margin.

const minCols = 2;

const calcNumColumns = (width) => {
  const cols = width / styles.item.width;
  const colsFloor = Math.floor(cols) > minCols ? Math.floor(cols) : minCols;
  const colsMinusMargin = cols - 2 * colsFloor * styles.item.margin;
  if (colsMinusMargin < colsFloor && colsFloor > minCols) return colsFloor - 1; 
  else return colsFloor;
};

If the resulting difference (colsMinusMargin) is smaller than the rounded one (colsFloor), this means that including our defined distance between the objects a maximum of colsFloor - 1 elements will fit in one row, otherwise colsFloor. The rest of the if ensures that we don't display fewer than our defined minCols columns.

Final fix

Now if we run this code like this and rotate our screen, we get an error message saying:

Changing numColumns on the fly is not supported. Change the key prop on FlatList when changing the number of columns to force a fresh render of the component.

This can be fixed by setting the Flatlist's key prop to numColumns.

Everything put together

Here you can finally try out for yourself how the flat list changes as a result of your own adjustments. If you want to rotate the screen or change the screen size, just open the app on your own mobile device or the Snack webplayer. After opening the link you can use the web player by clicking on the small icon to the right of the blue web button.

Conclusion

With that we have come to the end. Thanks for reading my first blog article. If you have any suggestions for improvement or questions, please leave a comment. Have fun with the Flatlist and see you soon!

Did you find this article valuable?

Support Fabian Grzesiak by becoming a sponsor. Any amount is appreciated!