The LayoutCommitObserver is a utility component that helps you track when all FlashList components in your component tree have completed their layout. This is particularly useful for coordinating complex UI behaviors that depend on list rendering completion.
Overview
When working with multiple FlashList components or when you need to perform actions after a FlashList has finished its render, the LayoutCommitObserver provides a clean way to observe and react to these layout events.
The callback is fired after every layout operation, not just the first one. This means it will be called whenever any FlashList in the tree commits a layout change.
When to Use
LayoutCommitObserver is ideal for:
- Measuring view sizes after all internal lists have rendered
- Coordinating animations that depend on list layout completion
- Parent components that don’t have direct access to FlashList (e.g., your component just accepts a
children prop)
- Multiple FlashLists where you need to know when all of them have completed layout
- Blocking paint until state changes are ready - doing
setState in the callback will block paint until your state change is committed
When NOT to Use
Consider alternatives in these scenarios:
- Single FlashList with access to it: Use the
onCommitLayoutEffect prop directly on FlashList instead
- Don’t need to block paint: Use the
onLoad callback for better performance
- Simple scroll tracking: Use
onViewableItemsChanged or scroll events instead
Basic Usage
Wrap your component tree containing FlashLists with LayoutCommitObserver:
import { LayoutCommitObserver, FlashList } from "@shopify/flash-list";
function MyScreen() {
const handleLayoutComplete = () => {
console.log("All FlashLists have completed their layout!");
// Perform any post-layout actions here
};
return (
<LayoutCommitObserver onCommitLayoutEffect={handleLayoutComplete}>
<View>
<FlashList
data={data1}
renderItem={renderItem1}
/>
<FlashList
data={data2}
renderItem={renderItem2}
/>
</View>
</LayoutCommitObserver>
);
}
Measuring Container Size
A common use case is measuring the size of a container after all lists have rendered:
import { useRef, useState, useCallback } from "react";
import { View, LayoutChangeEvent } from "react-native";
import { LayoutCommitObserver, FlashList } from "@shopify/flash-list";
function ResponsiveList() {
const containerRef = useRef<View>(null);
const [containerHeight, setContainerHeight] = useState(0);
const handleLayoutComplete = useCallback(() => {
// Measure container after lists have rendered
containerRef.current?.measure((x, y, width, height) => {
console.log("Container dimensions:", { width, height });
setContainerHeight(height);
});
}, []);
return (
<LayoutCommitObserver onCommitLayoutEffect={handleLayoutComplete}>
<View ref={containerRef}>
<FlashList
data={items}
renderItem={({ item }) => <ItemView item={item} />}
/>
</View>
</LayoutCommitObserver>
);
}
Coordinating Animations
LayoutCommitObserver is useful for triggering animations after lists have rendered:
import { useRef } from "react";
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
} from "react-native-reanimated";
import { LayoutCommitObserver, FlashList } from "@shopify/flash-list";
function AnimatedListContainer() {
const opacity = useSharedValue(0);
const hasRendered = useRef(false);
const handleLayoutComplete = () => {
if (!hasRendered.current) {
hasRendered.current = true;
// Fade in after initial render
opacity.value = withTiming(1, { duration: 300 });
}
};
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
}));
return (
<LayoutCommitObserver onCommitLayoutEffect={handleLayoutComplete}>
<Animated.View style={animatedStyle}>
<FlashList
data={items}
renderItem={({ item }) => <ItemView item={item} />}
/>
</Animated.View>
</LayoutCommitObserver>
);
}
Multiple Nested Lists
When working with nested FlashLists, LayoutCommitObserver ensures the callback fires only after all nested lists have completed:
import { LayoutCommitObserver, FlashList } from "@shopify/flash-list";
function NestedListsScreen() {
const handleAllLayoutsComplete = () => {
console.log("All nested lists have completed layout");
};
return (
<LayoutCommitObserver onCommitLayoutEffect={handleAllLayoutsComplete}>
<FlashList
data={sections}
renderItem={({ item: section }) => (
<View>
<Text>{section.title}</Text>
{/* Nested horizontal list */}
<FlashList
data={section.items}
horizontal
renderItem={({ item }) => <ItemCard item={item} />}
/>
</View>
)}
/>
</LayoutCommitObserver>
);
}
Blocking Paint for State Updates
LayoutCommitObserver can block paint until your state updates are ready:
import { useState } from "react";
import { LayoutCommitObserver, FlashList } from "@shopify/flash-list";
function SynchronizedUI() {
const [isReady, setIsReady] = useState(false);
const [measurements, setMeasurements] = useState({ width: 0, height: 0 });
const handleLayoutComplete = () => {
// This setState will block paint until committed
setMeasurements({ width: 100, height: 200 });
setIsReady(true);
};
return (
<LayoutCommitObserver onCommitLayoutEffect={handleLayoutComplete}>
<View>
{!isReady && <LoadingSpinner />}
<FlashList
data={items}
renderItem={({ item }) => <ItemView item={item} />}
/>
{isReady && <Footer measurements={measurements} />}
</View>
</LayoutCommitObserver>
);
}
Doing setState inside onCommitLayoutEffect can lead to infinite loops if not handled carefully. Make sure FlashList’s props are properly memoized to prevent continuous re-renders.
Preventing Infinite Loops
Be cautious when updating state in the callback:
import { useCallback, useMemo, useState } from "react";
import { LayoutCommitObserver, FlashList } from "@shopify/flash-list";
function SafeLayoutObserver() {
const [layoutCount, setLayoutCount] = useState(0);
const [data] = useState(initialData);
// ✅ Memoize callback to prevent infinite loops
const handleLayoutComplete = useCallback(() => {
setLayoutCount((prev) => prev + 1);
}, []);
// ✅ Memoize renderItem
const renderItem = useCallback(
({ item }) => <ItemView item={item} />,
[]
);
return (
<LayoutCommitObserver onCommitLayoutEffect={handleLayoutComplete}>
<FlashList
data={data}
renderItem={renderItem}
/>
</LayoutCommitObserver>
);
}
Comparison with onCommitLayoutEffect Prop
LayoutCommitObserver vs. FlashList’s onCommitLayoutEffect prop:
Using LayoutCommitObserver:
// ✅ Use when you don't have direct access to FlashList
function ParentComponent({ children }) {
return (
<LayoutCommitObserver onCommitLayoutEffect={() => console.log("Done")}>
{children}
</LayoutCommitObserver>
);
}
// Children render FlashList
function ChildComponent() {
}
Using onCommitLayoutEffect prop:
// ✅ Use when you have direct access to a single FlashList
function SimpleList() {
return (
<FlashList
data={data}
renderItem={renderItem}
onCommitLayoutEffect={() => console.log("Layout committed")}
/>
);
}
TypeScript Types
import type { LayoutCommitObserverProps } from "@shopify/flash-list";
interface LayoutCommitObserverProps {
children: React.ReactNode;
onCommitLayoutEffect?: () => void;
}
// Usage
const MyObserver: React.FC<LayoutCommitObserverProps> = ({
children,
onCommitLayoutEffect,
}) => {
return (
<LayoutCommitObserver onCommitLayoutEffect={onCommitLayoutEffect}>
{children}
</LayoutCommitObserver>
);
};
Props
The component tree that contains one or more FlashList components.
Callback invoked when all FlashList components in the tree have completed their layout. Called after every layout operation, not just the first render.
Best Practices
- Memoize callbacks: Always memoize the
onCommitLayoutEffect callback to prevent infinite loops
- Memoize FlashList props: Ensure
renderItem, keyExtractor, and other FlashList props are memoized
- Avoid heavy operations: Keep the callback lightweight - heavy operations should be debounced or moved to useEffect
- Use conditionally: Only use LayoutCommitObserver when you actually need to coordinate multiple lists or measure after layout
- Consider alternatives: If you have access to FlashList directly and only need to track one list, use
onCommitLayoutEffect prop instead
Common Use Cases
Use Case 1: Loading Indicator
function ListWithLoader() {
const [isLoading, setIsLoading] = useState(true);
return (
<>
{isLoading && <LoadingOverlay />}
<LayoutCommitObserver
onCommitLayoutEffect={() => setIsLoading(false)}
>
</LayoutCommitObserver>
</>
);
}
Use Case 2: Analytics
function TrackedList() {
const renderStartTime = useRef(Date.now());
const handleLayoutComplete = useCallback(() => {
const renderTime = Date.now() - renderStartTime.current;
analytics.track("list_rendered", { renderTime });
}, []);
return (
<LayoutCommitObserver onCommitLayoutEffect={handleLayoutComplete}>
</LayoutCommitObserver>
);
}
function ListWithAutoScroll() {
const flashListRef = useRef(null);
const shouldScroll = useRef(true);
const handleLayoutComplete = useCallback(() => {
if (shouldScroll.current) {
shouldScroll.current = false;
flashListRef.current?.scrollToIndex({
index: 10,
animated: false,
});
}
}, []);
return (
<LayoutCommitObserver onCommitLayoutEffect={handleLayoutComplete}>
<FlashList
ref={flashListRef}
data={data}
renderItem={renderItem}
/>
</LayoutCommitObserver>
);
}
Summary
LayoutCommitObserver tracks when all FlashList components in a tree complete their layout
- Use it when you need to coordinate actions across multiple lists or measure views after rendering
- Always memoize callbacks and FlashList props to prevent infinite loops
- Consider using
onCommitLayoutEffect prop directly on FlashList when you have access to a single list
- The callback fires after every layout operation, not just the first render
- Useful for animations, measurements, loading states, and analytics