This blog is part of the Blinkit React Native blog series. Through this series, we attempt to share the journey of integrating React Native into one of India’s largest e-commerce grocery web & native apps.
I joined Blinkit as an intern in June 2021, and I was assigned the task of adding adjacent views to our carousel. In this article, I will share how I added Adjacent Views in react-native-swiper. React-native-swiper is a popular carousel library within react-native, which lets us display items in a carousel with additional features like loop and pagination.
What Did I Work On?
In our new app revamp, we needed a carousel that supports adjacent views. We were using react-native-swiper, which didn’t have support for adjacent views at that time.
Like any other team, we didn’t want to reinvent the wheel, so we planned to patch this library instead. I was glad to be working on this because it was an exciting problem for me to solve.
React-native-swiper doesn’t support a carousel where we can display adjacent items to users, but we wanted a carousel to show the user adjacent carousel items.
But why do we want this behavior?
Talking from a business perspective, if the item takes the whole width of the device, the user will never be able to discover other items. Eventually, scrolling through a long list, sideways, will reduce our conversion rates as items would be hard to find. This was particularly true for customers trying to explore the platform. That’s why we wanted a carousel where we could nudge users about adjacent items. Carousels also work well when the customer wants to ‘just look around’, as interesting products are fewer steps away.
Previously, the slider used to look like this — each item takes the device’s width.
But now, we wanted a carousel where we can show the adjacent views of the carousel:
The action plan
Before introducing a new feature to the library, it’s crucial to learn about its inner workings. After learning the internal working of the react-native-swiper (aka swiper), I started ideating different ways our new feature could be introduced while simultaneously ensuring that this won’t break any existing features.
So, how did Swiper work initially?
- Swiper will create a duplicate element of the last element and put it at the very start of the list, and it will also create a duplicate of the first element and put it at the very end of the list.
- As Swiper has placed a duplicate of the carousel’s last item, our carousel would start from the same duplicate of the last element.
- But swiper does a neat trick where during onLayout (which gives us dimensions and position element and is invoked on mount and on layout changes), It will fire one manual scrollTo event to skip the duplicate of the last element and position the ScrollView to the real first element.
- Swiper would do a neat trick for its loop feature, as soon as the user scrolls left from 1 element to duplicate the last element, it will manually trigger a scroll and position it to the real last element.
- As you can check from both steps referred in images, when the user snaps left from the real first element to the duplicate of the last element. It will manually trigger scroll to the original last element. The same follows when the user scrolls right from the real last element to the duplicate of the first element.
Introducing the new feature
Now that we understand Swiper’s ‘working’, bringing a new feature to a library is not that easy because you want to ensure the existing features don’t break whilst providing proper abstraction. So every major part is offloaded to the library.
We introduced four new props (consider props as parameters to our render function) to the library
- showAdjacentViews: It determines whether we want to show adjacent views or not.
- adjacentViewsWidth: It determines how much width of the adjacent views should be displayed.
- adjacentViewsPadding: It works like a margin between elements
- decelerationRate: It determines how quickly the scroll view decelerates after the user lifts their finger.
So how will these props help us?
- When showAdjacentViews is true, we will calculate the new width of the item considering its width and padding (adjacentViewsWidth and adjacentViewsPadding).
- During the onLayout, we will calculate a new offset to skip the duplicate of the last element and alongside displaying its adjacentViewWidth port.
- decelerationRate props helps us to determine how quick scroll of the items should be when user lifts their finger from screen
Challenges that came along
Snapping of carousel item became janky and laggy
We were still not close to our expected behavior– For perfect snapping behavior, we need to make sure that the user lands appropriately to the new item alongside adjacentViewWidth ports. It was clear pagingEnabled would not solve our problem here but neither would snapToInterval, because it doesn’t respect the offset, which we manually scrolled during onLayout resulting in our views getting misplaced after a few snaps. That’s where snapToOffsets came to our rescue, as it ensures our snap positions are never disturbed.
Looping on a carousel would cause a weird readjust of items
So we managed to achieve snapping correctly but we weren’t finished yet–Whenever the user scrolls left from the first element to duplicate the last element, they will see the end of the screen. Then again manual scroll will be triggered to scroll to the actual last element but this time the user will experience weird readjusts.
As you can see from the images, this behavior feels very weird. So we came up with a clever solution which was to add one more duplicate of elements at both ends.
As you can see from the above image, we cleverly add one more duplicate element on both sides.
Edge case with this solution
I know you all might wonder what if the user scrolls hard and reaches the very end of ScrollView and the same problem of readjusting with weird UI will appear, that’s where thinking as a user instead of engineers comes to the place.
If the user scrolls hard, all the view elements will move out of the screen at a greater speed, which means the readjust would never catch the user’s eyes because their eyes were busy capturing the moving views. Also, the proper index would be always maintained if the user reaches to duplicate of the second last element. We will manually scroll to the actual second last element.
As shown above, if the user snaps hard and reaches the last element of the carousel. We will automatically position the carousel cursor to the original element.
Looping on the carousel would cause a flicker on screen
Well, lazy-loading (loading components only when required) is definitely important when we want to display a large amount of data. We thought we were done with our patch and our new feature was complete. So we started testing it, and we noticed when lazy-loading was enabled there were some weird flickers, turns out these flickers were none other than our lazy-fallbacks
So when the user scrolls to a duplicate of the last element, the actual last element is actually in the lazy-fallback state. This means when we manually scroll to the actual last element, it will be in its lazy-fallback state.
As you can see from the image when we scrolled to the actual element, for some seconds it was in a lazy-fallback state and then the user saw the actual elements.
So we added the last piece of our puzzle which was whenever the user was at any end of the list. We will force render two elements of the opposite end–this means if a user is at first element we will force render duplicate of the first element and actual last element, and vice-versa if a user is at the last element.
As you can see from the image, we cleverly force render duplicate elements based on carousel position.
So with this, I managed to pull this feature in react-native-swiper, and it wouldn’t have been possible without the team’s efforts.
All of these changes also work on vertical slider too.
Snack link showcasing adjacent views.
Github PR for adding this feature to the official repo.