A few months ago at Affinity, we decided to take the most-used feature in our web app, sheets (see the image below), and add it to our mobile app. This blog post explains the React Native limitations that we ran into and how we overcame them. If you’re having difficulties building a responsive table supporting frozen columns and rows with React Native, then please read ahead!

Our sheets view in our web app

What was our feature spec?

We needed to build a table view of our data that supported a frozen header row, a frozen column, and potentially hundreds of rows. This table view also needed to support a variety of interactions including editing cells, filtering, and sorting.

What does React Native offer?

FlatList: This component renders data in a scrolling container that provides many features out of the box such as lazy loading and frozen row/column support. It’s important to note that FlatList is a PureComponent , which means that it will not re-render if props remain shallow-equal. Although this is a very powerful component, it has its limitations. It makes performance optimizations only when used in a vertical orientation and provides support for a frozen row/column only in one direction based on the orientation (i.e. frozen row support if vertical orientation and frozen column support for horizontal orientation).

ScrollView: This is a simple React Native component that wraps elements in a scrollable view and renders all its elements at once. It provides support for frozen rows/columns like the FlatList, but does not offer any significant performance optimizations like lazy loading.

Animated: This is a React Native library that can be used to make animations more fluid and powerful. It’s very flexible — using a function called createAnimatedComponent , you can make any React Native component animatable and bind an animated value to a property on the component. The only downside to this library is that it’s not very intuitive to use.

How did we solve for our feature spec?

We went to Google for answers…

A lot of people had solutions using some combination of FlatList, ScrollView, and Animated to create a good experience for either a frozen column or frozen row, but there was no solution that supported both.

We then decided to look at existing apps that supported table views and potentially used React Native. We looked at both Airtable and Quip but realized that neither must use React Native as their solutions were different for iOS and Android.

Given what React Native provides and the solutions we saw online, we knew that we could leverage the frozen row/column support on ScrollView or FlatList for one scroll direction and implement the other direction ourselves.

Approach 1 (manually synced frozen column)

This initially seemed like the best approach because we could use a vertical FlatList and leverage all its performance improvements along with scroll loading. In order to support a frozen column, we created a ScrollView that contained all the information for the first column and bound its vertical scroll position to the vertical scroll position of the FlatList. FlatList has a onScroll prop that can be leveraged to get the current scroll position. This seemed to work great if there were only 15–20 rows loaded on screen but as soon as more than 30 rows were loaded, scrolling performance took a huge hit. We realized this might not be the best approach, especially because it is very important for our users to be able to see many rows (potentially hundreds).

Approach 2 (manually synced frozen row)

In order to leverage the frozen column support from FlatList, we had to set the orientation to horizontal which meant that we wouldn’t be getting the performance improvements or the scroll loading. Similar to above, we created a ScrollView that contained all the information for the first row and bound its horizontal scroll position to the horizontal scroll position of the FlatList. This seemed to work no matter how many rows we loaded, but the scroll performance took a hit if there were more than 30 columns shown. Our users rarely need to see more than a couple dozen columns at a time, so we decided that this was a much better solution for our use case. This approach seemed promising but wasn’t perfect so we tried to make it better.

Approach 2: Making it better

Although we had a rough solution, the horizontal scrolling position of the frozen row was slow to sync at times, especially if the user scrolled the table horizontally very fast. We tried a few tactics, like changing the scrollEventThrottle which controls how often the scroll event fires while scrolling. Eventually, after doing some research online, we realized we might be able to make this scroll transition smoother using React Native’s Animated library. By using an animated value for scroll position of the frozen row, the horizontal scrolling of the frozen row was much smoother and felt exactly in sync with the FlatList’s scroll position.

We still needed to implement scroll loading, so we decided to wrap both the header row and the body (a horizontal FlatList) inside a vertical FlatList so we could leverage scroll loading. Of course, this still means that we aren’t leveraging FlatList’s performance improvements, but we were happy with our results. You can test and use our solution by checking out this Expo snack!

Overall, this solution works well for us, but it has its drawbacks. Here are the main strengths and limitations as you look to use or build off of our solution:

Strengths:

smooth scrolling

frozen row and column implementation

Limitations:

no performance optimizations for vertical scrolling (all cells are rendered at all times)

table starts to lag once many rows are loaded

limitations on number of columns rendered (scrolling starts to degrade with over 20 columns after adding styling and interaction logic for each cell)

Conclusion

I’m sure there are a few ideas we didn’t consider and this solution could be better so please feel free to leave comments/ideas! I’d love to chat with anyone about our implementation.