5 - Listes


Boucles

Il est possible de construire un tableau d'objets JSX et de les afficher.

const items = [
    <Text>A</Text>,
    <Text>B</Text>,
    <Text>C</Text>,
];

// ...

return (
   <View style={{ flexDirection: 'row', justifyContent: 'space-around' }}>
   { items }
   </View>
);

On peut également exploiter les fonctions des tableaux, pour traiter des données et produire un tableau

const items = ["a", "b", "c"];

// ...

return (
    <View style={{ flexDirection: 'row', justifyContent: 'space-around' }}>
    { 
        items.map((i, index) => {
            return (
                <Text 
                    style={{ 
                        color: (index % 2 == 0 ? 'red' : 'blue')
                    }}
                >
                    { i }
                </Text>
            );
        })
    }
    </View>
);

Attention

  • Mauvaise performance avec beaucoup d'items
  • Gestion manuelle de la mise en page
return (
    <View style={{ flexDirection: 'row', gap: 16, justifyContent: 'center', flexWrap: 'wrap' }}>
    { 
        [...Array(99)].map(() => {
            return (
                <Text 
                    style={{ 
                        backgroundColor: 'lightgray',
                        padding: 16
                    }}
                >
                    { '🤓' }
                </Text>
            );
        })
    }
    </View>
);


Each child in a list should have a unique "key" prop

Pour permettre à React de synchroniser efficacement l'état et le rendu, chaque item d'une liste doit être identifié par une clé unique via la prop key.

<X key={ ... } />

📚 Keeping list items in order with key

ScrollView

La ScrollView permet de faire défiler le contenu s'il se retrouve à l'extérieur des limites de l'affichage.

  • Formulaires
  • Pages/Carrousels
  • Sélections

Pour une petite quantité d'items, car le rendu est calculé pour tous les items en même temps!

import { ScrollView } from 'react-native';

// ATTENTION contentContainerStyle pour controler la mise en page dans la ScrollView

// Tester nowrap et horizontal={ true }
return (
    <ScrollView contentContainerStyle={{ flexDirection: 'row', gap: 16, justifyContent: 'center', alignItems: 'center', flexWrap: 'wrap' }} >
    { 
        [...Array(99)].map(() => {
            return (
                <Text 
                    style={{ 
                        backgroundColor: 'lightgray',
                        padding: 16
                    }}
                >
                    { '🤓' }
                </Text>
            );
        })
    }
    </ScrollView>
);

Plusieurs personnalisations disponibles

  • direction
  • en-tête
  • pagination
  • défilement programmatique
  • pull to refresh
  • styles

Listes

Il existe 2 mécanismes pour une gestion performante de l'affichage en liste via React Native. Ceux-ci utilisent la virtualisation pour créer un ensemble réduit de cellules à afficher et les réutiliser au cours du défilement.

const numbers = [...Array(99)];

return (
    <FlatList
        data={ numbers }
        renderItem={ () => {
            return (
                <Text 
                    style={{ 
                        backgroundColor: 'lightgray',
                        padding: 16,
                        textAlign: 'center'
                    }}
                >
                { '🤓' }
                </Text>
            )
        }}
    />
);

On peut personnaliser le comportement

  • Extraire en component
  • Séparateur
  • En-tête, Pied de page
  • Liste vide
  • extraData combiné avec useState pour déclencher la mise à jour
function NumberItem({ value }) {
    return (
        <TouchableOpacity
            onPress={ () => console.log(`${value} pressed!`) }
        >
            <Text 
                style={{ 
                    padding: 8,
                    backgroundColor: 'lightgray',
                    textAlign: 'center',
                    fontSize: 24
                }}
            >
            { `${value} 🤓` }
            </Text>
        </TouchableOpacity>
    )
}

export default function App() {
    const numbers = [...Array(99)].map((_, index) => index);

    return (
        <FlatList
            data={ numbers }
            renderItem={ (listItem) => <NumberItem value={ listItem.item } /> }
            ItemSeparatorComponent={ () => <View style={{ height: 2, backgroundColor: 'black' }}/> }
        />
    );
}

🖥️ Todoer

Intégrer le mécanisme de liste dans le projet de départ ci-dessous.

  • FlatList

    • Component pour l'item
    • Séparateur
    • Component pour liste vide
    • Extraction de clé
  • [EXTRA] Tester la SectionList pour regrouper les items selon s'il sont complétés ou non

Projet de départ

/*
Dependencies

npx expo install react-native-safe-area-context react-native-root-toast expo-crypto

*/

import { useState } from 'react';
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Alert, View, ScrollView, TouchableHighlight, Switch, Button, Text, TextInput } from 'react-native';
import { randomUUID  as uuid } from 'expo-crypto';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import { RootSiblingParent } from 'react-native-root-siblings';
import Toast from 'react-native-root-toast';

function Input(props) {
    const { style, ...otherProps } = props;

    return (
        <TextInput 
            { ...otherProps } 
            style={[
                {
                    borderWidth: 1,
                    borderColor: 'lightgray',
                    padding: 8,
                },
                styles.input, style
            ]} 
        />
   );
}

function TodoView({ todo, toggle }) {

    function toggleCompleted() {
        Alert.alert(null, `${todo.name}`, [
            {
                text: 'Cancel',
                onPress: () => { },
                style: 'cancel',
            },
            {
                text: todo.done ? 'Incomplete' : 'Completed', 
                onPress: () => toggle(todo.id)
            },
        ]);
    }

    return (
        <TouchableHighlight key={todo} onPress={ toggleCompleted }>
        <View 
            style={{ 
                flex: 1,
                flexDirection: 'row',
                paddingVertical: 16, 
                paddingHorizontal: 8,
                backgroundColor: 'white', /* requis pour highlight */
                borderBottomColor: 'lightgray'
            }}
        >
            <Ionicons name="checkmark-circle" size={22} color={  todo.done ? 'green' : 'lightgray' } />

            <View style={{ flex: 1, flexDirection: 'column' }}>

                <Text style={{ fontSize: 18, fontWeight: 'bold' }}>{ todo.name }</Text>

                { 
                    todo.description &&
                    <Text style={{ fontSize: 18 }}>{ todo.description }</Text>
                }

            </View>

        </View>
        </TouchableHighlight>
    );
}


export default function App() {

    const EMPTY_TODO = () => { return {
        name: null,
        done: false,
        description: null
    }}

    const [todos, setTodos] = useState([]);
    const [newTodo, setNewTodo] = useState(EMPTY_TODO());

    function add() {
        if ((newTodo.name?.trim() ?? '') == '') {
            Toast.show('Provide a Todo name', {
                duration: Toast.durations.SHORT,
                backgroundColor: 'red',
                textColor: 'white',
            });
        } else {
            newTodo.id = uuid();
            setTodos([newTodo, ...todos]);
            setNewTodo(EMPTY_TODO());
        }
    }

    function toggle(id) {
        setTodos(
            todos.map( t => {
                if (t.id == id) {
                    return {
                        ...t, 
                        done : !t.done 
                    }
                }

                return t;
            })
        )
    }

    function list() {
        if (todos.length == 0) {
           return (
                <View style={{ flex: 1, justifyContent: 'center' }}>
                    <Text style={{ fontSize: 48, color: 'gray' }}>No todos...</Text>
                </View>
           )
        } else {
            return (
                <Text>{ JSON.stringify(todos) }</Text>
           )
        }
    }

    return (
    <>
        <StatusBar style="auto" />

        <RootSiblingParent>
        <SafeAreaProvider>
        <SafeAreaView style={styles.container}>

            <View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>

                <Input 
                    placeholder='New task'
                    style={{ height: 48, flexGrow: 1 }} 
                    value={ newTodo.name }
                    onChangeText={ (text) => setNewTodo({...newTodo, name: text}) }
                /> 

                <Switch 
                    value={ newTodo.done }
                    onValueChange={ checked => setNewTodo({...newTodo, done: checked}) } 
                />

            </View>

            <Input 
                placeholder='Optional description'
                style={{ width: '100%', height: 96, verticalAlign: 'top' }} 
                multiline={ true } 
                value={ newTodo.description }
                onChangeText={ (text) => setNewTodo({...newTodo, description: text}) }
            />

            <Button 
                title='Add' 
                color='green' 
                onPress={ add }
            />

            { list() }

        </SafeAreaView>
        </SafeAreaProvider>
        </RootSiblingParent>
    </>
    );
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        gap: 16,
        alignItems: 'center',
        padding: 16,
    },
});