Skip to main content

Section List

Section lists allow you to organize long lists of content by dividing them with headings.

Loading...

The primary component, SectionList, acts as the main orchestrator of the entire Section List interface. It coordinates the rendering of the table of contents and individual content sections.

Section List
const SectionList = () => {
const selectedItem = useSharedValue('');
const visibleIndex = useSharedValue(0);
const sectionNames = SECTIONS.map((s) => s.name);
const sectionCardsRef = useRef(null);
const tableOfContentsRef = useRef(null);

return (
<SafeAreaView style={sectionListStyles.cardsContainer}>
<TableOfContents
data={sectionNames}
visibleIndex={visibleIndex}
sectionCardsRef={sectionCardsRef}
tableOfContentsRef={tableOfContentsRef}
/>
<SectionCards
sections={SECTIONS}
visibleIndex={visibleIndex}
tableOfContentsRef={tableOfContentsRef}
sectionCardsRef={sectionCardsRef}
/>
</SafeAreaView>
);
};

Within SectionList, there are two key components: TableOfContents and SectionCards.

TableOfContents is responsible for rendering the list of section names as a table of contents. It receives props such as data, visibleIndex, sectionCardsRef, and tableOfContentsRef to manage navigation and synchronization between the table of contents and section content.

Section List
const TableOfContents = ({
data,
visibleIndex,
sectionCardsRef,
tableOfContentsRef,
}) => {
return (
<View style={sectionListStyles.tableOfContents}>
<FlashList
horizontal
showsHorizontalScrollIndicator={false}
renderItem={({ item, index }) => (
<TableOfContentsElement
item={item}
visibleIndex={visibleIndex}
index={index}
sectionCardsRef={sectionCardsRef}
/>
)}
data={data}
estimatedItemSize={52}
ref={tableOfContentsRef}
/>
</View>
);
};

SectionCards, on the other hand, manages the rendering of individual sections and their corresponding content. It receives props: sections, visibleIndex, sectionCardsRef, and tableOfContentsRef to render the content sections and handle scrolling interactions.

Section List
  sections,
visibleIndex,
sectionCardsRef,
tableOfContentsRef,
}) => {
const heights = sections.map((_) => SECTION_HEIGHT);

const getOffsetStarts = () =>
heights.map((v, i) => heights.slice(0, i).reduce((x, acc) => x + acc, 0));

const onScroll = (event) => {
const offset = event.nativeEvent?.contentOffset?.y;

if (offset !== undefined) {
const distancesFromTop = getOffsetStarts().map((v) =>
Math.abs(v - offset)
);
const newIndex = distancesFromTop.indexOf(
Math.min.apply(null, distancesFromTop)
);
if (visibleIndex.value !== newIndex) {
tableOfContentsRef.current?.scrollToIndex({
index: newIndex,
animated: true,
});
}
visibleIndex.value = newIndex;
}
};

const renderItem = ({ item }) => {
return (
<View>
<Text style={sectionCardStyles.header}> {item.name}</Text>
<SectionCardsElement>
<Text style={sectionCardStyles.content}>{item.content}</Text>
</SectionCardsElement>
</View>
);
};

return (
<View style={sectionListStyles.flex}>
<FlashList
ref={sectionCardsRef}
estimatedItemSize={52}
estimatedFirstItemOffset={0}
renderItem={renderItem}
data={sections}
onScrollBeginDrag={onScroll}
onScrollEndDrag={onScroll}
onScroll={debounce(onScroll)}
onMomentumScrollBegin={onScroll}
onMomentumScrollEnd={onScroll}
/>
</View>
);
};

The onScroll in SectionCards calculates the offset as the user scrolls through the content and determines which section is currently most visible on the screen. It is done by comparing the distance of each section from the top of the screen - it identifies the section closest to the viewport's top edge.


const getOffsetStarts = () =>
heights.map((v, i) => heights.slice(0, i).reduce((x, acc) => x + acc, 0));

const onScroll = (event) => {
const offset = event.nativeEvent?.contentOffset?.y;

if (offset !== undefined) {
const distancesFromTop = getOffsetStarts().map((v) =>
Math.abs(v - offset)
);
const newIndex = distancesFromTop.indexOf(
Math.min.apply(null, distancesFromTop)
);
if (visibleIndex.value !== newIndex) {
tableOfContentsRef.current?.scrollToIndex({
index: newIndex,
animated: true,
});
}
visibleIndex.value = newIndex;
}
};

We use the useSharedValue hook to create mutable shared values across different components. For instance, selectedItem and visibleIndex are shared values used to manage the currently selected section and its visibility index.

  const selectedItem = useSharedValue('');
const visibleIndex = useSharedValue(0);

Additionally, we use useAnimatedStyle hook to define animated styles based on the shared values. Then, we apply these animated styles to components to create dynamic visual effects, such as changing font weights and adding bottom borders.

  useAnimatedStyle(() => ({
fontWeight: selectedItem.value === item ? '600' : '400',
borderBottomWidth: selectedItem.value === item ? 1 : 0,
}));

To enable interaction with the FlashList component - such as scrolling to specific sections, the code utilizes variables created using useRef such as sectionCardsRef and tableContentsRef

  const sectionCardsRef = useRef(null);
const tableOfContentsRef = useRef(null);

Here, the debounce function throttles the invocations of onScroll event handler which improves the perfomrance.

function debounce(func, timeout = 100) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
func(...args);
}, timeout);
};
}