Kurs React Native · Moduł 4: Stan i dane z API

Context API - Globalne Skarby

3 min czytania ·
W tej lekcji4

Context API pozwala dzielić dane między komponentami bez "prop drilling" - przekazywania propsów przez wiele poziomów.

Tworzenie Context

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

// Typy
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 z wartością domyślną
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>
  );
}

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

Użycie w aplikacji

// 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>
  );
}

// Dowolny komponent w drzewie
function TreasureList() {
  const { treasures, totalValue, removeTreasure } = useTreasure();

  return (
    <View>
      <Text>Wartość skarbca: {totalValue} złota</Text>
      <FlatList
        data={treasures}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <Pressable onPress={() => removeTreasure(item.id)}>
            <Text>{item.name} - {item.value} złota</Text>
          </Pressable>
        )}
      />
    </View>
  );
}

// Inny komponent
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="Nazwa skarbu" />
      <Pressable onPress={handleAdd}>
        <Text>Dodaj skarb</Text>
      </Pressable>
    </View>
  );
}

Optymalizacja z useMemo i useCallback

import { useMemo, useCallback } from 'react';

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

  // Memoizacja wartości obliczanych
  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]);

  // Memoizacja funkcji
  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));
  }, []);

  // Memoizacja wartości kontekstu
  const value = useMemo(() => ({
    treasures,
    totalValue,
    legendaryCount,
    addTreasure,
    removeTreasure,
  }), [treasures, totalValue, legendaryCount, addTreasure, removeTreasure]);

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

Wiele Context'ów

// Rozdzielenie odpowiedzialności
<AuthProvider>
  <ThemeProvider>
    <TreasureProvider>
      <NotificationProvider>
        <App />
      </NotificationProvider>
    </TreasureProvider>
  </ThemeProvider>
</AuthProvider>

Context API świetnie sprawdza się dla stanu globalnego jak autoryzacja, motyw czy ustawienia.

Kod do tej lekcji: 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: stwórz Context z wartością domyślną undefined
const TreasureContext = createContext<TreasureContextType | undefined>(undefined);

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

  const addTreasure = (name: string) => {
    // TODO: dodaj nowy skarb do tablicy z unikalnym id (Date.now().toString())
    // setTreasures(prev => [...prev, { id: Date.now().toString(), name }]);
  };

  return (
    // TODO: udostępnij wartość { treasures, addTreasure } przez Provider
    <TreasureContext.Provider value={{ treasures, addTreasure }}>
      {children}
    </TreasureContext.Provider>
  );
}

// TODO: stwórz custom hook useTreasure, który czyta kontekst
// i rzuca błąd, gdy jest używany poza Providerem
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('Perła')}>
        <Text style={styles.buttonText}>Dodaj perłę</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 },
});

Hasła z dokumentacji w tej lekcji

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jak utworzyć nowy Context w React?

  2. 2. Co robi Context.Provider?

Zadania praktyczne w grze

  • Układanie w poziomie

    Ułóż poprawne użycie hooka useContext:

  • Edytor kodu

    Uzupełnij kod TreasureProvider i custom hooka useTreasure.

Przydatne artykuły