Cours React Native · Module 4: État et données d'API

L'API Context - Des trésors globaux

3 min de lecture ·
Dans cette leçon4

L'API Context permet de partager des données entre les composants sans « prop drilling » - c'est-à-dire sans faire passer les props à travers de nombreux niveaux.

Créer un Context

// contexts/TreasureContext.tsx
import { createContext, useContext, useState, useReducer, ReactNode } from 'react';

// Types
interface Treasure {
  id: string;
  name: string;
  value: number;
  rarity: 'common' | 'rare' | 'legendary';
}

interface TreasureContextType {
  treasures: Treasure[];
  totalValue: number;
  addTreasure: (treasure: Omit<Treasure, 'id'>) => void;
  removeTreasure: (id: string) => void;
  clearAll: () => void;
}

// Context avec une valeur par défaut
const TreasureContext = createContext<TreasureContextType | undefined>(undefined);

// Provider
export function TreasureProvider({ children }: { children: ReactNode }) {
  const [treasures, setTreasures] = useState<Treasure[]>([]);

  const totalValue = treasures.reduce((sum, t) => sum + t.value, 0);

  const addTreasure = (treasure: Omit<Treasure, 'id'>) => {
    const newTreasure: Treasure = {
      ...treasure,
      id: Date.now().toString(),
    };
    setTreasures(prev => [...prev, newTreasure]);
  };

  const removeTreasure = (id: string) => {
    setTreasures(prev => prev.filter(t => t.id !== id));
  };

  const clearAll = () => {
    setTreasures([]);
  };

  return (
    <TreasureContext.Provider
      value={{ treasures, totalValue, addTreasure, removeTreasure, clearAll }}
    >
      {children}
    </TreasureContext.Provider>
  );
}

// Hook personnalisé
export function useTreasure() {
  const context = useContext(TreasureContext);
  if (!context) {
    throw new Error('useTreasure must be used within TreasureProvider');
  }
  return context;
}

Utilisation dans l'application

// App.tsx
import { TreasureProvider } from './contexts/TreasureContext';

export default function App() {
  return (
    <TreasureProvider>
      <NavigationContainer>
        <Stack.Navigator>
          <Stack.Screen name="Home" component={HomeScreen} />
          <Stack.Screen name="Vault" component={VaultScreen} />
        </Stack.Navigator>
      </NavigationContainer>
    </TreasureProvider>
  );
}

// N'importe quel composant de l'arbre
function TreasureList() {
  const { treasures, totalValue, removeTreasure } = useTreasure();

  return (
    <View>
      <Text>Valeur du coffre : {totalValue} d'or</Text>
      <FlatList
        data={treasures}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <Pressable onPress={() => removeTreasure(item.id)}>
            <Text>{item.name} - {item.value} d'or</Text>
          </Pressable>
        )}
      />
    </View>
  );
}

// Un autre composant
function AddTreasureForm() {
  const { addTreasure } = useTreasure();
  const [name, setName] = useState('');

  const handleAdd = () => {
    addTreasure({ name, value: 100, rarity: 'common' });
    setName('');
  };

  return (
    <View>
      <TextInput value={name} onChangeText={setName} placeholder="Nom du trésor" />
      <Pressable onPress={handleAdd}>
        <Text>Ajouter un trésor</Text>
      </Pressable>
    </View>
  );
}

Optimisation avec useMemo et useCallback

import { useMemo, useCallback } from 'react';

export function TreasureProvider({ children }: { children: ReactNode }) {
  const [treasures, setTreasures] = useState<Treasure[]>([]);

  // Mémoïsation des valeurs calculées
  const totalValue = useMemo(() => {
    return treasures.reduce((sum, t) => sum + t.value, 0);
  }, [treasures]);

  const legendaryCount = useMemo(() => {
    return treasures.filter(t => t.rarity === 'legendary').length;
  }, [treasures]);

  // Mémoïsation des fonctions
  const addTreasure = useCallback((treasure: Omit<Treasure, 'id'>) => {
    setTreasures(prev => [...prev, { ...treasure, id: Date.now().toString() }]);
  }, []);

  const removeTreasure = useCallback((id: string) => {
    setTreasures(prev => prev.filter(t => t.id !== id));
  }, []);

  // Mémoïsation de la valeur du contexte
  const value = useMemo(() => ({
    treasures,
    totalValue,
    legendaryCount,
    addTreasure,
    removeTreasure,
  }), [treasures, totalValue, legendaryCount, addTreasure, removeTreasure]);

  return (
    <TreasureContext.Provider value={value}>
      {children}
    </TreasureContext.Provider>
  );
}

Plusieurs Contexts

// Séparation des responsabilités
<AuthProvider>
  <ThemeProvider>
    <TreasureProvider>
      <NotificationProvider>
        <App />
      </NotificationProvider>
    </TreasureProvider>
  </ThemeProvider>
</AuthProvider>

L'API Context est parfaite pour l'état global comme l'authentification, le thème ou les paramètres.

Code de cette leçon : App.tsx
import React, { createContext, useContext, useState, ReactNode } from 'react';
import { View, Text, Pressable, FlatList, StyleSheet } from 'react-native';

interface Treasure {
  id: string;
  name: string;
}

interface TreasureContextType {
  treasures: Treasure[];
  addTreasure: (name: string) => void;
}

// TODO : crée le Context avec une valeur par défaut undefined
const TreasureContext = createContext<TreasureContextType | undefined>(undefined);

function TreasureProvider({ children }: { children: ReactNode }) {
  const [treasures, setTreasures] = useState<Treasure[]>([]);

  const addTreasure = (name: string) => {
    // TODO : ajoute un nouveau trésor au tableau avec un id unique (Date.now().toString())
    // setTreasures(prev => [...prev, { id: Date.now().toString(), name }]);
  };

  return (
    // TODO : fournis la valeur { treasures, addTreasure } via le Provider
    <TreasureContext.Provider value={{ treasures, addTreasure }}>
      {children}
    </TreasureContext.Provider>
  );
}

// TODO : crée un hook personnalisé useTreasure qui lit le context
// et qui lève une erreur s'il est utilisé en dehors du Provider
function useTreasure() {
  const context = useContext(TreasureContext);
  if (!context) {
    throw new Error('useTreasure must be used within TreasureProvider');
  }
  return context;
}

function TreasureScreen() {
  const { treasures, addTreasure } = useTreasure();

  return (
    <View style={styles.container}>
      <Pressable style={styles.button} onPress={() => addTreasure('Perle')}>
        <Text style={styles.buttonText}>Ajouter une perle</Text>
      </Pressable>
      <FlatList
        data={treasures}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => <Text style={styles.item}>{item.name}</Text>}
      />
    </View>
  );
}

export default function App() {
  return (
    <TreasureProvider>
      <TreasureScreen />
    </TreasureProvider>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 20, backgroundColor: '#1a5276' },
  button: {
    paddingVertical: 14,
    borderRadius: 8,
    backgroundColor: '#2980b9',
    alignItems: 'center',
    marginBottom: 16,
  },
  buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
  item: { color: '#aed6f1', fontSize: 18, paddingVertical: 6 },
});

Entrées de la documentation utilisées dans cette leçon

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Comment crée-t-on un nouveau Context en React ?

  2. 2. Que fait Context.Provider ?

Tâches pratiques dans le jeu

  • Tri horizontal

    Reconstitue l'utilisation correcte du hook useContext :

  • Éditeur de code

    Complète le code du TreasureProvider et du hook personnalisé useTreasure.

Articles utiles