Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions src/components/mobile/LessonCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
View,
Text,
ScrollView,
FlatList,
Dimensions,
StyleSheet,
TouchableOpacity,
Expand Down Expand Up @@ -47,7 +48,7 @@ export default function LessonCarousel({
onLastLessonNext,
isLastLessonInSection = false,
}: LessonCarouselProps) {
const scrollViewRef = useRef<ScrollView>(null);
const scrollViewRef = useRef<FlatList>(null);
const [currentIndex, setCurrentIndex] = useState(0);
const progressBarWidth = useRef(new Animated.Value(0)).current;

Expand Down Expand Up @@ -78,8 +79,8 @@ export default function LessonCarousel({

const scrollToIndex = (index: number, animated = true) => {
if (scrollViewRef.current) {
scrollViewRef.current.scrollTo({
x: index * SCREEN_WIDTH,
scrollViewRef.current.scrollToOffset({
offset: index * SCREEN_WIDTH,
animated,
});
}
Expand Down Expand Up @@ -194,8 +195,10 @@ export default function LessonCarousel({
</View>

{/* Swipeable Content */}
<ScrollView
<FlatList
ref={scrollViewRef}
data={lessons}
keyExtractor={item => item.id}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
Expand All @@ -206,9 +209,13 @@ export default function LessonCarousel({
snapToInterval={SCREEN_WIDTH}
snapToAlignment="center"
contentContainerStyle={styles.scrollContent}
>
{lessons.map((lesson, index) => (
<View key={lesson.id} style={[styles.lessonContainer, { width: SCREEN_WIDTH }]}>
getItemLayout={(_, index) => ({ length: SCREEN_WIDTH, offset: SCREEN_WIDTH * index, index })}
maxToRenderPerBatch={10}
windowSize={5}
initialNumToRender={2}
removeClippedSubviews={true}
renderItem={({ item: lesson }) => (
<View style={[styles.lessonContainer, { width: SCREEN_WIDTH }]}>
<ScrollView
style={styles.lessonScrollView}
contentContainerStyle={styles.lessonContent}
Expand All @@ -217,8 +224,8 @@ export default function LessonCarousel({
{renderLessonContent(lesson)}
</ScrollView>
</View>
))}
</ScrollView>
)}
/>

{/* Navigation Buttons */}
<View style={styles.navigationContainer}>
Expand Down
26 changes: 16 additions & 10 deletions src/components/mobile/MobileQuizManager/QuizCarousel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect, useRef } from 'react';
import { Dimensions, ScrollView, StyleSheet, View } from 'react-native';
import { Dimensions, ScrollView, FlatList, StyleSheet, View } from 'react-native';
import { Question } from '../../../types/course';
import MobileQuestionCard from './MobileQuestionCard';

Expand All @@ -25,15 +25,15 @@ export default function QuizCarousel({
onQuestionChange,
onAnswerSelect,
}: QuizCarouselProps) {
const scrollViewRef = useRef<ScrollView>(null);
const scrollViewRef = useRef<FlatList>(null);
const isScrollingRef = useRef(false);
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);

useEffect(() => {
// Only scroll if not currently being scrolled by user
if (scrollViewRef.current && !isScrollingRef.current) {
scrollViewRef.current.scrollTo({
x: currentQuestionIndex * SCREEN_WIDTH,
scrollViewRef.current.scrollToOffset({
offset: currentQuestionIndex * SCREEN_WIDTH,
animated: true,
});
}
Expand Down Expand Up @@ -84,8 +84,10 @@ export default function QuizCarousel({

return (
<View style={styles.container}>
<ScrollView
<FlatList
ref={scrollViewRef}
data={questions}
keyExtractor={question => question.id}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
Expand All @@ -99,9 +101,13 @@ export default function QuizCarousel({
snapToAlignment="center"
contentContainerStyle={styles.scrollContent}
bounces={false}
>
{questions.map((question, index) => (
<View key={question.id} style={[styles.cardContainer, { width: SCREEN_WIDTH }]}>
getItemLayout={(_, index) => ({ length: SCREEN_WIDTH, offset: SCREEN_WIDTH * index, index })}
maxToRenderPerBatch={10}
windowSize={5}
initialNumToRender={2}
removeClippedSubviews={true}
renderItem={({ item: question, index }) => (
<View style={[styles.cardContainer, { width: SCREEN_WIDTH }]}>
<MobileQuestionCard
question={question}
questionNumber={index + 1}
Expand All @@ -110,8 +116,8 @@ export default function QuizCarousel({
onAnswerSelect={onAnswerSelect}
/>
</View>
))}
</ScrollView>
)}
/>
</View>
);
}
Expand Down
5 changes: 5 additions & 0 deletions src/components/mobile/MobileSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,11 @@ export const MobileSearch = ({
ListEmptyComponent={
<Text style={styles.emptyText}>Try a different query or adjust filters.</Text>
}
getItemLayout={(_, index) => ({ length: 120, offset: 120 * index, index })}
maxToRenderPerBatch={15}
windowSize={5}
initialNumToRender={10}
removeClippedSubviews={true}
/>
</View>
)}
Expand Down
38 changes: 21 additions & 17 deletions src/components/mobile/MobileSyllabus.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import {
Platform,
ScrollView,
FlatList,
StyleSheet,
Text,
TouchableOpacity,
Expand Down Expand Up @@ -80,27 +80,31 @@ export default function MobileSyllabus({
};

return (
<ScrollView
<FlatList
data={sections}
keyExtractor={section => section.id}
style={styles.container}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
>
{/* Header */}
<View style={styles.header}>
<Text style={styles.headerTitle}>📚 Course Syllabus</Text>
<Text style={styles.headerSubtitle}>
{sections.length} sections • {sections.reduce((acc, s) => acc + s.lessons.length, 0)}{' '}
lessons
</Text>
</View>

{/* Sections */}
{sections.map(section => {
maxToRenderPerBatch={10}
windowSize={5}
initialNumToRender={10}
removeClippedSubviews={true}
ListHeaderComponent={
<View style={styles.header}>
<Text style={styles.headerTitle}>📚 Course Syllabus</Text>
<Text style={styles.headerSubtitle}>
{sections.length} sections • {sections.reduce((acc, s) => acc + s.lessons.length, 0)}{' '}
lessons
</Text>
</View>
}
renderItem={({ item: section }) => {
const isExpanded = expandedSections.has(section.id);
const sectionProgress = getSectionProgress(section);

return (
<View key={section.id} style={styles.sectionCard}>
<View style={styles.sectionCard}>
{/* Section Header */}
<TouchableOpacity
onPress={() => toggleSection(section.id)}
Expand Down Expand Up @@ -207,8 +211,8 @@ export default function MobileSyllabus({
)}
</View>
);
})}
</ScrollView>
}}
/>
);
}

Expand Down
89 changes: 89 additions & 0 deletions tests/components/Performance.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import MobileSyllabus from '../../src/components/mobile/MobileSyllabus';
import QuizCarousel from '../../src/components/mobile/MobileQuizManager/QuizCarousel';
import LessonCarousel from '../../src/components/mobile/LessonCarousel';
import { MobileSearch } from '../../src/components/mobile/MobileSearch';

// Mock course data for tests
const mockSections = [
{
id: 's1',
title: 'Section 1',
lessons: Array.from({ length: 100 }).map((_, i) => ({
id: `l${i}`,
title: `Lesson ${i}`,
content: 'Content',
duration: 10,
})),
},
];

const mockQuestions = Array.from({ length: 100 }).map((_, i) => ({
id: `q${i}`,
text: `Question ${i}`,
type: 'multiple-choice' as const,
options: ['A', 'B', 'C', 'D'],
correctAnswer: 'A',
}));

describe('List Performance Optimization', () => {
it('MobileSyllabus uses FlatList with proper optimization props', () => {
const { getByType } = render(
<MobileSyllabus
sections={mockSections}
onLessonSelect={() => {}}
/>
);
const flatList = getByType('FlatList' as any);
expect(flatList.props.maxToRenderPerBatch).toBe(10);
expect(flatList.props.windowSize).toBe(5);
expect(flatList.props.initialNumToRender).toBe(10);
expect(flatList.props.removeClippedSubviews).toBe(true);
});

it('QuizCarousel uses FlatList with proper optimization props', () => {
const { getByType } = render(
<QuizCarousel
questions={mockQuestions}
currentQuestionIndex={0}
selectedAnswers={{}}
onQuestionChange={() => {}}
onAnswerSelect={() => {}}
/>
);
const flatList = getByType('FlatList' as any);
expect(flatList.props.maxToRenderPerBatch).toBe(10);
expect(flatList.props.windowSize).toBe(5);
expect(flatList.props.initialNumToRender).toBe(2);
expect(flatList.props.removeClippedSubviews).toBe(true);
expect(flatList.props.getItemLayout).toBeDefined();
});

it('LessonCarousel uses FlatList with proper optimization props', () => {
const { getByType } = render(
<LessonCarousel
lessons={mockSections[0].lessons}
currentLessonId="l0"
onLessonChange={() => {}}
renderLessonContent={() => <></>}
/>
);
const flatList = getByType('FlatList' as any);
expect(flatList.props.maxToRenderPerBatch).toBe(10);
expect(flatList.props.windowSize).toBe(5);
expect(flatList.props.initialNumToRender).toBe(2);
expect(flatList.props.removeClippedSubviews).toBe(true);
expect(flatList.props.getItemLayout).toBeDefined();
});

it('MobileSearch uses FlatList with proper optimization props', () => {
const { getByType } = render(<MobileSearch />);
const flatList = getByType('FlatList' as any);
expect(flatList.props.maxToRenderPerBatch).toBe(15);
expect(flatList.props.windowSize).toBe(5);
expect(flatList.props.initialNumToRender).toBe(10);
expect(flatList.props.removeClippedSubviews).toBe(true);
expect(flatList.props.getItemLayout).toBeDefined();
});
});
Loading