4 - Interactivité


Projet de départ

En respectant la structure vue précédemment, créer les 2 écrans suivants

Form.js

import { View, TextInput, Text, Switch, Button } from 'react-native';

export default function Form() {
    return (
        <View style={{ flex: 1, gap: 16, justifyContent: 'center', alignItems: 'center' }}>
            <TextInput 
                placeholder="Name" 
            />

            <View style={{ flexDirection: 'row', alignItems: 'center' }}>
                <Switch />

                <Text>Dark mode?</Text>
            </View>

            <Button 
                title="Show!"
            />

        </View>
    );
}

Show.js

import { View, Text, Button } from 'react-native';

export default function Show() {
    return (
        <View style={{ flex: 1, gap: 16, justifyContent: 'center', alignItems: 'center' }}>

            <Text>Show</Text>

            <Button 
                title="Again?"
            />

        </View>
    );
}

Événements

En consultant la documentation des components qu'on utilise(Button, TextInput, Switch, Librairie, etc.) on constate un type de prop commençant par on..., ex: onPress. C'est la nomenclature standard pour identifier les événements.

On peut ensuite s'inscrire pour être informé du déclenchement de l'événement via JSX et exécuter le comportement désiré.

TextInput onChangeText

<TextInput 
    placeholder="Name" 
    onChangeText={ (text) => console.log(text) }
/>

On utilise ici la forme inline de l'inscription à l'événement. () => ... représente la notation raccourcie des fonctions fléchées.

Button onPress

export default function Form() {

    function pressed() {
        console.log('pressed!');
    }

    return (
        {/* ... */}

        <Button 
            title="Show!"
            onPress={ pressed }
        />

        {/* ... */}
    );
}

On peut également envoyer en référence une fonction traditionnelle.

On ne peut pas exécuter directement notre code, il faut absolument passer par une fonctions. L'événement appelera cette fonction en fournissant les arguments appropriés.

State

Le state permets au component de conserver un état au fil temps. Autrement, chaque fois que le component de met à jour, donc render, la fonction est appelée sans traces des variables qu'elle contenait précédemment. React offre un mécanisme pour stocker de l'information qui sera accessible d'un render à l'autre.

C'est cet état qui permets à React de suivre l'évolution du component et de constater les changements qui demande une mise à jour. Attention les données d'état sont immutables, on ne doit pas changer directement la valeur d'une variable, mais plutôt assigner une copie contenant la nouvelle valeur. C'est ce changement de référence qui indique à React de re-render.

📚 Plusieurs changements

📚 Avec les objets

📚 Avec les tableaux

Switch

import { useState } from 'react';

export default function Form() {

    const [checked, setChecked] = useState(false);

    console.log(checked);

    return (
        {/* ... */}

        <Switch 
            value={ checked }
            onValueChange={ (on) => setChecked(on) }
        />

        {/* ... */}
    );
}

Pour suivre plusieurs valeurs reliées, il est intéressant d'encapsuler l'état dans un objet.

Form.js

import { View, TextInput, Text, Switch, Button } from 'react-native';
import { useState } from 'react';

export default function Form() {

    const [display, setDisplay] = useState({});
    // {} VS { name: '', darkMode: false }

    console.log(display);

    return (
        {/* ... */}

        <TextInput 
            placeholder="Name" 
            value={ display.name }
            onChangeText={ (text) => {

                setDisplay({...display, name: text}); // Destructuring pour creer la copie

                console.log(`onChangeText: ${display.name}`); // ATTENTION

            }}
        />

        {/* ... */}

        <Switch 
            value={ display.darkMode }
            onValueChange={ (on) => setDisplay({...display, darkMode: on}) }
        />

        {/* ... */}
    );
}

Attention La nouvelle valeur assignée n'est pas disponible immédiatement dans le même cycle de rendu.

Toasts/Alertes

Les Toasts permettent d'afficher un message qui disparait automatiquement. Ils sont natifs sur Android mais pas iOS, la librairie Root Toast permets d'uniformiser ce comportement.

Validation nom requis Form.js

import { RootSiblingParent } from 'react-native-root-siblings';
import Toast from 'react-native-root-toast';

export default function Form() {

    function pressed() {
        if (display.name.trim() == '') {
            Toast.show('Provide a name', {
                duration: Toast.durations.SHORT,
                backgroundColor: 'red',
                textColor: 'white',
            });
        }
    }

    return (
        <RootSiblingParent>
            <View style={{ flex: 1, gap: 16, justifyContent: 'center', alignItems: 'center' }}>

                {/* ... */}

            </View>
        </RootSiblingParent>
    );
}


Les alertes sont un boîte de dialogue intéractive contenant un titre, un message optionnel et jusqu'à 3 boutons.

Button Show.js

import { View, Text, Button, Alert } from 'react-native';

export default function Show() {

    function againPressed() {
        Alert.alert('Title', 'Message', [
            {
                text: 'A',
                onPress: () => console.log('pressed a')
            },
            {
                text: 'B',
                onPress: () => console.log('pressed b')
            },
            {
                text: 'C', 
                onPress: () => console.log('pressed c')
            },
        ]);
    }

    {/* ... */}
}

Navigation

📚 Configurer React Navigation

npx expo install @react-navigation/native @react-navigation/native-stack react-native-screens react-native-safe-area-context

App.js

import { View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import Form from './Form.js';
import Show from './Show.js';

const Stack = createNativeStackNavigator();

export default function App() {
    return (
        <View style={{ flex: 1, backgroundColor: 'teal' }}>
            <NavigationContainer>
                <Stack.Navigator initialRouteName="Form">

                    <Stack.Screen
                        name="Form"
                        component={ Form }
                        options={{ title: 'Welcome' }}
                    />

                    <Stack.Screen name="Show" component={ Show } />

                </Stack.Navigator>
            </NavigationContainer>
        </View>
    );
}

Pour déclencher la navigation, chaque component utilisé en tant que Screen reçoit une prop navigation. navigate(name) permet d'aller vers une nouvelle page, ou de retourner sur une page existante. Par défaut, si on est déjà sur la page, la navigation n'est pas redéclenchée.

Form.js

export default function Form({ navigation }) {

    function pressed() {
        if (display.name.trim() == '') {
            // ...
        } else {
            navigation.navigate('Show');
        }
    }

    // ...
}

On peut fournir des paramètre lors de la navigation.

Form.js

navigation.navigate('Show', display);

Show.js

import { View, Text, Button, Alert } from 'react-native';

export default function Show({ route }) {

    console.log(`Show ${JSON.stringify(route.params)}`);
    const display = route.params;

    return (
        <View 
            style={{ 
                flex: 1, gap: 16, justifyContent: 'center', alignItems: 'center',
                backgroundColor: (display.darkMode ? 'black' : 'white')
            }}
        >

            <Text style={{ color: (display.darkMode ? 'white' : 'black') }}>
                { display.name }
            </Text>

            {/* ... */}
        </View>
    );
}

📚 Navigation Params

Plusieurs options de configuration disponibles, soit via le NavigationContainer ou directement dans les écrans enfants

  • Titre
  • Boutons
  • Style

App.js

import { StatusBar } from 'expo-status-bar';
import { View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import Form from './Form.js';
import Show from './Show.js';

const Stack = createNativeStackNavigator();

export default function App() {
    return (
        <>
            <StatusBar style="light" />

            <View style={{ flex: 1, backgroundColor: 'red' }}>
            {/* View wrapper si backgroundColor dans les components enfants */}
                <NavigationContainer>
                    <Stack.Navigator 
                        initialRouteName="Form"
                        screenOptions={{
                            headerStyle: {
                                backgroundColor: 'seagreen'
                            },
                            headerTintColor: 'white',
                        }}
                    >

                        <Stack.Screen
                            name="Form"
                            component={ Form }
                            options={{ title: 'Welcome' }}
                        />

                        <Stack.Screen name="Show" component={ Show } />

                    </Stack.Navigator>
                </NavigationContainer>
            </View>
        </>
    );
}

Show.js

import { StatusBar } from 'expo-status-bar';
import { View, Text, Button, Alert } from 'react-native';

export default function Show({ navigation, route }) {

    console.log(`Show ${JSON.stringify(route.params)}`);
    const display = route.params;

    navigation.setOptions({
        headerTintColor: display.darkMode ? 'black' : 'white',
    });

    function againPressed() {
        Alert.alert('Title', 'Message', [
            {
                text: 'Pop(to root)',
                onPress: () => console.log('pressed a')
            },
            {
                text: 'Nav(back)',
                onPress: () => console.log('pressed b')
            },
            {
                text: 'Push(new)', 
                onPress: () => console.log('pressed c')
            },
        ]);
    }

    return (
        <>
            <StatusBar style={ display.darkMode ? 'dark' : 'light' } />

            <View 
                style={{ 
                    flex: 1, gap: 16, justifyContent: 'center', alignItems: 'center',
                    backgroundColor: (display.darkMode ? 'black' : 'white')
                }}
            >

                <Text style={{ color: (display.darkMode ? 'white' : 'black') }}>
                    { display.name }
                </Text>

                <Button 
                    title="Again?"
                    onPress={ againPressed }
                />

            </View>
        </>
    );
}

📚 Stack Navigator Options

Il existe 4 principaux modes de navigation pour les Stack

  • navigate
  • push
  • pop, popToTop
  • replace

Show.js

export default function Show({ navigation, route }) {
    {/* ... */}

    function againPressed() {
        Alert.alert('Title', 'Message', [
            {
                text: 'Pop(root)',
                onPress: () => navigation.popToTop()
            },
            {
                text: 'Nav(reuse)',
                onPress: () => navigation.navigate('Form')
            },
            {
                text: 'Push(new)', 
                onPress: () => navigation.push('Form')
            },
        ]);
    }

    {/* ... */}
}

Lorsqu'on retourne sur un écran, on peut recevoir des données de navigation

Show.js

navigation.navigate('Form', { name: '' })

Form.js

import { useState, useEffect } from 'react';

export default function Form({ navigation, route }) {
    {/* ... */}

    console.log(route.params);

    useEffect(() => {
        console.log(`effect ${JSON.stringify(route.params)}`);

        if (route.params) {
            setDisplay({ ...display, name: route.params.name });
        }
    }, [route.params]);

    {/* ... */}
}

📚 React Navigation

Effect

useEffect est un mécanisme de React permettant de réagir au changement d'un état, state, en vue de synchroniser avec un élément externe: requête HTTP, timeout/interval, NavigationContainer, etc.

Le second paramètre de la fonction est les dépendances déclenchant l'effet

  • absent, chaque render
  • [], seulement 1 fois, au premier render
  • [stateA, stateB, ...], lors d'un changement d'état

Show.js

import { useEffect } from 'react';

export default function Show({ navigation, route }) {
    {/* ... */}

    useEffect(() => {
        navigation.setOptions({
            headerTintColor: display.darkMode ? 'black' : 'white',
        });
    }, [])

    {/* ... */}
}

📚 useEffect