React Native Scrollview 101: The Best Practices Guide
When mobile apps abruptly clip off content from the screen, it’s a sign of bad UX for developers. Moreover, not being able to scroll through your app’s screen could be confusing for your users. Luckily, scrolling containers provide a reliable solution to this problem.
This post presents a deep and comprehensive guide on React Native’s scrolling containers, which are called ScrollViews. You’ll understand what they are, write them in code, and explore some best practices with practical examples.
What’s Wrong With the View Component?
The most generic container used in React Native is the <View> component. You can use it to build complex UIs by nesting native as well as custom components like <Text>, <Image>, <TouchableOpacity>, etc.
However, it doesn’t account for overflow. As a result, when you render content that goes out of the bounds of a user’s screen, <View> will clip off all the overflowing content. Even with a fixed height around your containers, the content might overflow on devices with small screens.
ScrollView to the Rescue
ScrollView is a scrollable container that can nest one or more components inside it. It accounts for vertical as well as horizontal scrolling and gives a native scrolling experience to your users. Whenever your screen’s UI cannot be contained at a fixed height, you should implement a ScrollView.
Additionally, it gives a bunch of useful features as props. Let’s dive deeper into understanding them with code.
Creating a React Native Project Using Expo
If you’re new to Expo, you can get started with it here. Create a new Expo project by running
Navigate into the project directory to jump-start your Expo project.
This will open the Expo project in your browser with a QR code that you can scan from the Expo app on your smartphone. Then, you can directly run your React Native app on your smartphone with live reloading.
For brevity, I’ll write all the code for this example inside App.js only. I also use a dummy image that you can download to follow me step by step.
The below code uses a <View> component as a generic container to wrap some <Text> and an <Image> component. It also has some styles to give some margins and padding to the <View> components.
Notice how the last <Text> component abruptly clips off at the end of the screen:
If this was your app, your user wouldn’t be able to see the last line, as it gets cropped out of the screen. Also, the vertical margin at the bottom of the screen is barely visible. Your user might attempt to scroll down to view the full content but will not be able to do so.
To address this issue, replace the outermost <View> with a <ScrollView>. Make sure you also import it at the top.
Notice how the vertical margin from the bottom appears on the screen.
There’s also a thin, gray scrollbar that appears when you start to scroll down to see the entire content.
That’s it! That’s how simple it is to use a ScrollView in your React Native app. Let’s look at a practical example to explore it further.
Building a Scrolling Profile Screen Using ScrollView
Most apps always have a dedicated profile screen where the authenticated user can see their details. Since profile screens can sometimes have a lot of content to display, you build them using a ScrollView.
Let’s build a simple and scrollable profile screen that has the following:
- A sticky header with a back button
- User’s profile picture, username, name, and bio
- User-related stats such as followers, projects, etc.
- Some action buttons to follow the user, connect to the user’s Instagram account, etc.
- User’s posts fetched from external API
- Pull-to-refresh functionality
To get started, create the following files and folders:
Creating Global Style Sheet
Inside the GlobalStyles.js folder, add the following lines of code to describe some minimal styles for your buttons:
These styles will be used across various components on the profile screen.
Creating the ProfileHeader Component
The ProfileHeader component displays a back button and a title. The back button utilizes the global styles created in the previous section.
Creating the Posts Component
The Posts component (./Posts/Posts.js) takes in some posts as props. It then renders <Image> nested inside a <View> for each of the posts.
Creating the ActionButtons Component
The ActionButtons component (./Profile/ActionButtons.js) renders some buttons as <TouchableOpacity>, along with some styles and icons.
Creating the Avatar Component
Next, output the user’s profile picture inside Avatar.js. The source URL of the image is also taken as prop inside this component. You can destructure the image property to directly use it inside URI of the <Image> component.
Creating the ProfileInformation Component
Render the <Avatar> component and include user-specific details like name, username, number of followers, etc. from the data object. You can destructure data directly from props as shown below.
Putting It All Together
Now that all the individual components for the profile screen are ready, let’s put them together. To begin with, create a constant to store all the dummy information about the user.
The user’s posts will come from an API. Since fetching data from an API is an asynchronous task, you need states to store this data. Create a state variable called userData that stores all user-related information. Also, create two more states to handle loading and pull to refresh.
The refresh state indicates if data fetching from pull to refresh action is underway. It will be used later to implement the pull to refresh functionality. The loading state represents whether data fetching is completed or is in progress.
Next, create a getUserData() function, which makes an API call to the endpoint https://picsum.photos/v2/list?page=2&limit=5 that returns five images in its response. Combine this response with data created earlier in the userData state.
Finally, render ProfileHeader.js inside the <ScrollView>. Based on the loading state, conditionally render the remaining profile components. Remember to pass down the relevant properties of userData as props. Your entire Profile.js should look like this:
If you’ve done everything correctly, you should see a minimal template for a user profile screen built using ScrollView. Looks pretty neat, right? You can scroll all the way down to see a user’s post, as well.
By default, ScrollView renders its children in a vertical scrolling list. You can, however, pass the horizontal prop to your <ScrollView> component to render a horizontal list instead. You can read more about it here.
Adding a Sticky Header in ScrollView
ScrollView inherently supports sticky headers using the stickyHeaderIndices prop. This prop takes in an object with an array index. The index denotes the index of the child component inside the <ScrollView> component that’s supposed to be rendered sticky.
If you look at everything the <ScrollView> component renders, you’ll notice it has four child components underneath it. Also, <ProfileHeader> is the 0th child among all the children components. Hence, you can make this component sticky by assigning the stickyHeaderIndices prop to be , as shown below.
Once you’ve done that, your ScrollView will scroll through the entire screen with the profile header remaining sticky at the top!
Adding Pull to Refresh in a ScrollView
A lot of apps have a pull-to-refresh functionality in screens where data is changing rapidly. It allows users to engage more with their app and eases the pain for developers of implicitly updating content on a screen.
ScrollView takes a refreshControl prop that takes in a component. This component is responsible for the UI and functionality of your ScrollView’s pull-to-refresh action. React Native provides a native RefreshControl component that handles this for you. Import RefreshControl at the top:
Add this component to the refreshControl prop:
The RefreshControl component takes two props:
- onRefresh (function)
- refreshing (boolean)
When the user performs the pull-to-refresh action, <ScrollView>‘s RefreshControl renders the RefreshControl component. As a result, RefreshControl shows a small spinner at the top indicating the action is in progress and fires the onRefresh function. The visibility of the spinner is controlled by the refreshing prop.
Your onRefresh function should essentially make a fresh API call to fetch the data. It should also update your userData state accordingly. Since you already have a getUserData() function that does this, you can make adjustments to it to incorporate the refresh action. Here’s how the updated getUserData() function should look:
It takes the parameter isRefresh, which is false by default. When this function is invoked via the refresh action, isRefresh is passed true, as in the above case. Inside this function, you also set the corresponding value of the refresh state to indicate that the data fetching for refresh action is in progress.
Your ScrollView should now have a pull-to-refresh functionality with only a few additional lines of code!
The thin, gray scrollbar doesn’t look that nice on native devices. In fact, most apps hide the scrollbar as a better UI practice. You can remove the scrollbar or scrolling indicator from <ScrollView> using the showsVerticalScrollIndicator prop. Simply pass false inside the showsVerticalScrollIndicator prop, as shown below:
And voila! <ScrollView> now renders without any scrolling indicators.
React Native’s official docs clearly mention that “ScrollViews must have a bounded height in order to work.”
This means that if your ScrollView is rendered inside a <View>, the parent <View> must have a bounded height. Therefore, you must assign a fixed height to your <ScrollView>. However, this might create unresponsive layouts on devices with varying screen sizes.
A workaround to this is assigning height dynamically with respect to the device’s screen width or height. React Native provides a native module called Dimensions that dynamically gets a device’s width and height.
If you have multiple parent <View> components rendering your <ScrollView>, you can give flex:1 to all your nested <Views> down the stack. This ensures that your ScrollView renders and behaves as intended.
Performance Downside of ScrollView
<ScrollView> is a useful container component, but you shouldn’t blindly use it everywhere. When you’re rendering a large amount of data in a single go, <ScrollView> isn’t the best choice. The simplest explanation for that is <ScrollView> renders all your child components at once. If you’re rendering large and heavy UI, your <ScrollView> will cause a render lag the first time it renders on the screen.
To overcome this performance downside, React Native provides the <FlatList> component. <FlatList> renders your child components lazily. This leads to a smooth UI rendering, offering a better user experience. You can read more about <FlatList>, its usage, and advantage over <ScrollView> here.
Testing is an integral part of developing robust and bug-free applications. Let’s create a simple ScrollView and write some unit tests using Jest and react-test-renderer.
Install jest-expo as a development dependency by running the following command:
Also, install react-test-renderer:
Following Expo’s official docs for testing with Jest and react-test-renderer, add the following configuration changes to your package.json file:
Inside the root folder of your project, create a new folder ScrollViewTest with the following files:
Inside data.js, I have added some sample data from a free public endpoint that the <ScrollView> component can render. For brevity, I am only using the first 10 items in the response returned by the API.
Inside ScrollViewTest.js, I simply render the above data.
I’ll write three tests for the <ScrollViewTest> component that ensures the following:
- A single parent component is rendered
- The parent component is a <ScrollView>
- <ScrollView> renders all the items defined by data.js
From the root directory, run the following command to run your test:
If you’ve done everything correctly, all your tests should pass with flying colors!
As your apps become larger and more complex, writing unit or UI tests might not be enough. Moreover, if you don’t have a dedicated testing team, you might ship your features involving ScrollViews with potential bugs. Waldo provides a simple no-code testing platform to run automated UI tests directly in your browser. It also intelligently detects bottlenecks in your code to fix issues conveniently.
TheScrollView is a piece of art if used correctly. Remember to use it as a generic container wherever you don’t need to render heavy UI. The examples demonstrated in this post should be enough to get you started, but you can explore ScrollView further by reading through the official documentation.
There are loads of interesting features that you can implement using some useful props. For instance, you can scroll to particular elements inside your ScrollView, add pagination, provide custom styling to your scrolling indicators, and much more.