react-native: "VirtualizedLists should never be nested inside plain ScrollViews with the same orientation" error in console for FlatList/SectionList with scrollEnabled={false}

Description

We use FlatList and SectionList in various sub-components which we embed into a master component in two ways:

  1. the components are in tabs of a tab control so each sub-component scrolls the FlatList or SectionList so has the benefit of using virtualization - we ensure scrollEnabled={true} in this case,
  2. the components are rendered one under the other in a single ScrollView - we ensure scrollEnabled={false} in this case,

We get the “VirtualizedLists should never be nested inside plain ScrollViews with the same orientation” console error repeatedly in the case of 2. I think when scrollEnabled={false} it should turn off virtualization and suppress this console error, and render the list internally using standard maps. Yes I can build my own wrapper components to do this, but I think the framework should do it naturally.

React Native version:

System: OS: macOS 11.3.1 CPU: (8) x64 Intel® Core™ i7-4770HQ CPU @ 2.20GHz Memory: 388.60 MB / 16.00 GB Shell: 3.2.57 - /bin/bash Binaries: Node: 15.1.0 - /usr/local/bin/node Yarn: 1.19.1 - ~/.yarn/bin/yarn npm: 7.0.8 - /usr/local/bin/npm Watchman: 4.9.0 - /usr/local/bin/watchman Managers: CocoaPods: 1.10.1 - /usr/local/bin/pod SDKs: iOS SDK: Platforms: iOS 14.5, DriverKit 20.4, macOS 11.3, tvOS 14.5, watchOS 7.4 Android SDK: API Levels: 23, 28, 29, 30 Build Tools: 23.0.1, 23.0.2, 25.0.0, 25.0.1, 25.0.2, 26.0.2, 26.0.3, 27.0.0, 27.0.3, 28.0.2, 28.0.3, 29.0.2, 29.0.3 System Images: android-16 | Google APIs Intel x86 Atom, android-22 | Google APIs Intel x86 Atom, android-28 | Google APIs Intel x86 Atom, android-29 | Google APIs Intel x86 Atom, android-29 | Google APIs Intel x86 Atom_64 Android NDK: Not Found IDEs: Android Studio: 4.1 AI-201.8743.12.41.7199119 Xcode: 12.5/12E262 - /usr/bin/xcodebuild Languages: Java: 1.8.0_271 - /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/bin/javac npmPackages: @react-native-community/cli: Not Found react: 17.0.1 => 17.0.1 react-native: 0.64.1 => 0.64.1 react-native-macos: Not Found npmGlobalPackages: react-native: Not Found

Steps To Reproduce

As per the description.

Expected Results

Would expect not to see the console error on a FlatList or SectionList which has scrollEnabled={false}.

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 14
  • Comments: 24

Commits related to this issue

Most upvoted comments

🎖 Simple Solution To Fix It: 100% works 🤩

1- Add nestedScrollEnabled={true} to ScrollView 2- Add scrollEnabled={false} to FlatList

<ScrollView nestedScrollEnabled={true} >
     <FlatList
            scrollEnabled={false}
            ...
        />
 </ScrollView>

Directly find the source code and comment out the console error

path: react-native/Libraries/Lists/VirtualizedList.js line: 1135

if (__DEV__) {
      ret = (
        <ScrollView.Context.Consumer>
          {scrollContext => {
            if (
              scrollContext != null &&
              !scrollContext.horizontal ===
                !horizontalOrDefault(this.props.horizontal) &&
              !this._hasWarned.nesting &&
              this.context == null
            ) {
              // TODO (T46547044): use React.warn once 16.9 is sync'd: https://github.com/facebook/react/pull/15170
              // console.error(
              //   'VirtualizedLists should never be nested inside plain ScrollViews with the same ' +
              //     'orientation because it can break windowing and other functionality - use another ' +
              //     'VirtualizedList-backed container instead.',
              // );
              this._hasWarned.nesting = true;
            }
            return innerRet;
          }}
        </ScrollView.Context.Consumer>
      );
    }

Hello,

I have the same error, and the proposed solutions do not suit me very well… Is it possible to disable this error when scrollEnabled={false} ? If the scroll is disabled, I don’t see how it can be a problem to use a FlatList in a ScrollView.

@KarthikeyanM28 I have read a number of posts on the web, and kind of found some explanation for this warning. I am typing from what I can remember. I think I have found at some point someone pointing to the native source code of the scrollable component behind the ScrollView and FlatList components and explaining what happens, but I cannot find that post right now…

Before I start, I haven’t found an official RN team answer as to why this warning and what the actual reason is for it.

Anyway, the story was something like this: The scrollable component source code that is behind the RN scrollables has a difficult time identifying whether it should steal and handle the touch events, or to let it pass-through. The problem is that this component was built similar to a singleton pattern, where one, and only one, of such components ever exists in the tree branch it is used in - you can have adjacent branches with this component, but no nested sub-branches.

The difficulty of the scrollable component when nesting multiple instances in sub-branches is that the instances will fight each other as to which one is eligible for stealing the touch events. Usually, this is resolved according to the event bubbling phase order (which flows bottom-up, child-to-parent), but the zIndex and the capture phase events (which flow top-down, parent-to-child, and before the bubbling phase) can screw this up. Not to mention that some other components (such as TouchableXXX) can also fight for the touch events (stack overflow has a number of posts on how to fix a ScrollView stealing touches from TouchableXXX childs).

Due to this flaw in the scrollable component, many issues arise:

  • the already mentioned scrollables stealing touches from touchables
  • nested TextInputs not being scrollable themselves
  • multiple scrollables scrolling at the same time and in the same direction (this one, I must say, it’s interesting for a parallax effect, though, it is definitely an undesired side-effect).

There are also many component layout and sizing issues due to this. The nature of the scrollable components is to render dynamically sized content. This makes layout calculations hard for RN since it has to continuously bounce between parents and childs to calculate the layouts, until all resolved dimensions will stabilize. { height: "100%" } (or width for horizontal scrollables) or { flexGrow: 1 } can make for a really troubling case where the scrollable content size grows, then child’s size grows as well, causing the content size to grow again, and this process repeating infinitely until the app crashes.

The issue is somewhat non-conflicting when limiting nested scrollables to 2 instances and using different scrollable directions. Scrollables have a gesture distance activation threshold (if I remember correctly, there must be a delta of +/-10dp) in the direction observed before the scrollable will deem itself eligible for stealing the touch events. Since the horizontal and vertical directions look for deltas on different axes, they do not interfere with each other that much.
This explains the ... with the same orientation part of the warning. However, I have had a number of instances where both scrolling directions would activate at the same time, making it difficult for the user to follow the intended touch gesture. Again, nest one more with the same orientation, and you’re back to the issues presented above.

As far as I could conclude from my findings on the web, the React Native team solution was… well, this warning.
They did not enforce this rule to completely prevent you from nesting scrollables, at any depth and with whichever orientation you desire, but they are also not supporting use cases of nested scrollables with the same orientation.
The consequence is that reporting a bug for misbehavior in these cases will not be their responsibility - it is not supported. Also, I do not think that they have any plans at this time to write a proper scrollable component that would behave correctly.

It is worth mentioning however that nesting scrollables with a different orientation is a supported use case, as also confirmed by the nestedScrollEnabled prop required to enable nested scrolling on Android (now enabled by default). Also, if I am not mistaken, the problem of this scrollable component also had to do with differences in scroll handling and behavior between iOS and Android.

So, if you’re wondering about possible solutions:

  • LogBox.ignoreLogs(['VirtualizedLists should never be nested']);
    Workaround. And then you should test a lot between iOS and Android to make sure that the behavior is consistent and does not fall apart.
  • As the warning says, do not nest scrollables with the same orientation (and, I would add, do not do this deeper that 1 parent and 1 child). Try to stay as much as possible within the RN limitations, and use the available scrollable components and their props to your advantage:
    • use ScrollView for randomly sized and arranged components
    • use FlatList for homogeneous components, but without visual grouping of sort
    • use SectionList for homogeneous components organized by groups
    • use VirtualizedList for a combination of ScrollView/FlatList/SectionList -like behavior, but where you actually need a lot of control over the item rendering and size calculation
    • Use ListHeaderComponent and ListFooterComponent to render dynamic items at the beginning and end of FlatList or SectionList. These components do not have to be the same form or size as the rest of the items rendered in the list.
    • Disable scroll when the content size is smaller than the container size, to prevent scroll events from interfering with each other. This can possibly improve the user experience as the scroll will stay fixed as much as possible
class AutoDisableScrollable extends React.PureComponent {
	state = { container: 0, content: 0 };

	onLayout = (layoutEvent) => {
		const { nativeEvent: { layout: { width, height } } } = layoutEvent;
		this.setSize("container", width, height, () => {
			if (typeof (this.props.onLayout) === "function") {
				this.props.onLayout(layoutEvent);
			}
		});
	}

	onContentSizeChange = (width, height) => {
		this.setSize("content", width, height, () => {
			if (typeof (this.props.onLayout) === "function") {
				this.props.onContentSizeChange(width, height);
			}
		});
	}

	setSize = (ofItem, width, height, callback) => {
		const currentSize = this.state[ofItem];
		const newSize = (this.props.horizontal !== true) ? height : width;

		if (newSize !== currentSize) {
			this.setState({ [ofItem]: newSize }, callback);
		} else {
			callback();
		}
	}

	render = () => {
		const {
			__ref, // Use this instead of the "ref" prop
			ScrollableComponent, // ScrollView, FlatList, SectionList, VirtualizedList
			scrollEnabled, bounces, onLayout, onContentSizeChange, children, ...props
		} = this.props;
		
		const { container, content } = this.state;
		const __scrollEnabled = ((scrollEnabled !== false) && (content > container));
		const __bounces = ((bounces !== false) && (__scrollEnabled === true));

		return <ScrollableComponent
			ref={__ref}
			scrollEnabled={__scrollEnabled}
			bounces={__bounces}
			onLayout={this.onLayout}
			onContentSizeChange={this.onContentSizeChange}
			{...props}
		>
			{children}
		</ScrollableComponent>;
	}
}
<AutoDisableScrollable
    ScrollableComponent={ScrollView}
    style={{ flex: 1 }}
    ...
>
    ...
</AutoDisableScrollable>

I hope that this wall of text helps you and others to get scrollables working correctly. I do have ideas for a custom scrollable component that uses Animated.Views and a PanResponder to handle touches and “scroll”, however, I have not materialized this yet. In case you want to dig into this, a container View would use a PanResponder to respond to touch events, and in given conditions, would accept handling touches and update the translateX / translateY props of a child content container Animated.View to “scroll” the items (the approach is similar to how scrollables in RN are built).

I personnaly changed ScrollView to a FlatList, which brought me to the same result:

<FlatList
	data={[{}]}
	keyExtractor={() => null}
	renderItem={() => <>{children}</>}
/>

A solution for me was to create a custom FlatList component, something like this

import React from 'react'
import { FlatList, FlatListProps } from 'react-native'

import { uniqueId } from 'lodash'

/** This component was created to avoid the Warning about nested virtualized lists when nesting a flatlist inside a scrollview */

const FlatListScrollView = <T extends any>({ children, ...props }: React.PropsWithChildren<Partial<FlatListProps<T>>>) => {
  return (
    <FlatList
      data={[undefined] as any}
      showsVerticalScrollIndicator={false}
      renderItem={() => <React.Fragment>{children}</React.Fragment>}
      keyExtractor={(_, index) => uniqueId('flat-list-scroll-view-element-' + index)}
      initialNumToRender={1}
      maxToRenderPerBatch={1}
      {...props}
    />
  )
}

export default FlatListScrollView

Any update on this issue? Inserting one or multiple FlatLists inside a ScrollView, even with scrolling inside the lists being disabled, sounds like a common thing to me. It avoids having to .map() our data lists ourselves and brings a few other benefits from the FlatList component. And I don’t think “remove the error from the source code yourself” is a correct solution either 👀

Solution: do(scolled Enabled = false) child component

Yes the fix is here: https://github.com/facebook/react-native/commit/62f83a9fad027ef0ed808f7e34973bb01cdf10e9

One-liner to just supress the error when scrollEnabled is false. Which is sensible. I think this also means we’re safe to ignore this error if we know scrollEnabled is false, even before we upgrade to RN 0.71

thanks for the fix @annepham25