An exercise that started out as an effort to replicate a screen from one of the most used social media applications by leveraging on the React Native framework turned out to be a lot more work than I had originally anticipated. I assumed all I had to do was to slap a few React Native elements together with their appropriate styling and “Tada!”, I could applaud myself for a job well done. Little did I know of the impeding bottleneck patiently awaiting me.
Eager to complete the necessary work as quickly as I could, I dived right in to constructing a mental model of how the layout should be structured. This is my intuitive approach to implementing designs; I never go wrong with breaking complex designs into fundamental core components needed to achieve desired layout.
I concluded on an approach which required nesting FlatLists. In total, there were four of them.
- One grandparent vertical FlatList where the three remaining FlatLists and other necessary components will be nested.
- One child horizontal FlatList where the remaining two Flatlists will be nested directly in. This enables easy horizontal scrolling between the other nested lists.
- The last two grandchildren FlatLists are vertical lists where the image items are nested directly in.
I implemented the mockup immediately and the gotcha became obvious. The problem with this approach was that Scrolling of the nested vertical FlatLists’ content was impeded when the pan was initiated directly in the grandparent vertical FlatList region and vice-versa. It became apparent that my traditional approach to implementing the layout was most likely not achievable. I spent hours trying to research ways to circumvent these limitations but every suggestion introduced a certain trade-off in the desired UX.
To provide more context into the desired UX, a scroll in the composite component should be able to pan any available content in the direction to which it is being moved, continuously; regardless of whichever specific FlatList the content was nested directly in.
Having nested vertical FlatLists with impeded scrolling during content panning was not an acceptable bargain; hence I was prompted to take a not so little detour of building a list that could seamlessly fulfil my layout and UX requirements.
NestedLists
Prior to this exercise I had very limited experience using gesture responders. Although I had basic understanding of React Native’s Gesture Responder System, I had no experience using the more elaborate React Native Gesture Handler. I dived straight into their documentations to get myself armored up to defeat my enemies, I couldn’t afford to be put to shame by mere inanimate objects. 😅
One of my first impressions was the vast difference in ease of usage of the React Native Gesture Handler in comparison to React Native’s Gesture Responder System. One less obvious impression was how React Native Gesture Handler’s seamless integration with React Native Reanimated transformed fluidity and complexity of animations. Hence, I concluded on leveraging on the power boost from the duo. Armed to the teeth with weapons of mass destruction, I proceeded on the journey to build NestedLists. 😂
NestedLists Features
- Its layout is structured to have two regions, a header region where the “grandparent FlatList” content (a.k.a NestedLists header) is rendered and a nested lists region where the “grandchildren FlatLists” (a.k.a Nested Lists) are rendered.
- Scrolling between the nested lists is achieved by a horizontal swipe in the nested lists region of the component. Panning the entire lists content must complete seamlessly between regions without any restriction in movement.
- Scrolling to the NestedLists header region from any of the nested lists, scrolls other nested lists to the top of their content.
- Each nested list’s height is solely dependent on the height of its own content
- Refreshable - nested lists content should be refreshable
- Dynamic content loading – nested lists should be able to load more content upon scroll to end.
- Scalable - NestedLists should exhibit features specified above efficiently regardless of the number of nested lists.
Using React Native Reanimated and React Native Gesture Handler, I created animated values (a.k.a Shared Values) which reacted to gestures and drove the animation of Animated Views during scrolling. The Animated Views contained components of NestedLists and were transformed using their “translateX” and “translateY” style property upon update of their animated values.
Using reactive hooks from React Native Reanimated such as “useAnimatedReaction” and “useDerivedValue”, I updated animated values when conditions for their update were met. These conditional updates were key to ensuring the desired UX was achievable.
Performance
To test the performance of NestedLists, I completed my implementation of Instagram’s profile screen and checked if it fulfilled the requirements it was built for. My observations were:
- It loads more content dynamically by triggering the “onEndReached” prop provided to it when it reaches towards the end of the list.
- It calls the “onRefresh” prop provided to the list, refreshing the active nested list’s content upon trigger.
- It has as an optional nested lists sticky header which remain visible at the top of the nested lists, when the NestedLists Header have been panned out of view.
- It provides an optional nested lists footer which can be used to indicate loading of more content when necessary.
- It also fulfilled the desired layout and UX requirements earlier stated.
I also wondered how performant it functioned so I tracked the JS and UI thread FPS ratings of the Instagram profile screen implementation, using the Expo Go App on an iPhone 7 (to test on low-end devices). I monitored its ratings while the app actively performed operations such as fetching data from remote sources, storing or manipulation of data and rendering of components. Its performance rating on the UI thread was consistently within 60 FPS, the lowest it dropped to after initial mount was 57 FPS. The JS thread was however inconsistent in its ratings. During basic actions such as scrolling through the list content, it consistently reported a rating around 60FPS. The rating however dropped to as low as 30 FPS during rendering of content, manipulation of data, navigation and other JS thread related activities. I was however unable to get any conclusive evidence that indicates the performance lag was indeed caused by the NestedLists implementation.
One other major limitation that flawed NestedLists is its lack of virtualization. Virtualization is an important feature of a performant list as it optimizes resources to maximize performance while rendering list content as necessary. My incomplete understanding of the behind the scenes logic or the optimal approach to designing such a feature is why I ignored it during the implementation of NestedLists.
Conclusion
I enjoyed every bit of trying to recreate one of React Native’s core components as it helped enhance my understanding of how React Native is able to wield the wonderful weapon of deploying applications for multiple platforms without breaking a sweat, well, maybe just a little, okay a lot of sweat 💀. The purpose of this writeup is to open myself to feedback, I am curious about what fellow React Native warriors think about my implementation. Was this a pointless venture with rewards that could have been sowed through less excruciating means? Should I even be trying to reinvent the wheels?
I have linked the repo to my Instagram profile screen implementation, it includes the source code for NestedLists.
Please comment your feedback on what or how I could have done better.
Top comments (5)
I'm certain going the extra mile to implement this without going into "Package Hell" and searching for ready-made solutions gave you a new level of insight and critical understanding of how React Native works. Especially how the UI/JS Thread interact and the performance bottlenecks one might encounter.
Even if there are already existing solutions and you may have unknowingly "reinvented the wheel", this must have been a quite impactful experience.
It definitely was an impactful experience. Thank your for the good feedback
My initial thought approach was to just create the header in a view and then use a top tabs to render the grandchildren flatlist then wrap everything in a scrollView. Thats where the first battle started. The top tabs refused to scroll along with the ScrollView.
After weeks of trying to find a solution to it I came up with the same brilliant idea you used of using a parent Flatlist to accomplish the vertical scroll and then another FlatList that contained the two grandchildren FlatList, but then I came across the same problem you did. I was so happy when I read your article cause all the while it was like my approach to solving the problem was out of this world and then there was an easier way to accomplish this. Thank you!
I am glad, you found this helpful. Thank you for reading :)
I’m encountering an issue where the horizontal scroll in the Stories section isn’t functioning as expected . Could you kindly assist me in resolving this problem? Your guidance would be greatly appreciated.