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

Projekt: Skarbiec Atlantydy - Kompletna aplikacja

4 min czytania ·
W tej lekcji5

Połączmy wszystkie poznane techniki w kompletny projekt!

Struktura projektu

src/
├── api/
│   ├── client.ts         # Konfiguracja Axios
│   └── treasureService.ts
├── stores/
│   ├── authStore.ts      # Zustand - autoryzacja
│   └── settingsStore.ts  # Zustand - ustawienia
├── hooks/
│   ├── useTreasures.ts   # React Query hooks
│   └── useAuth.ts
├── screens/
│   ├── HomeScreen.tsx
│   ├── TreasureListScreen.tsx
│   └── TreasureDetailScreen.tsx
└── components/
    ├── TreasureCard.tsx
    └── LoadingSpinner.tsx

API Client z refreshem tokenu

// api/client.ts
import axios from 'axios';
import { useAuthStore } from '@/stores/authStore';

const apiClient = axios.create({
  baseURL: 'https://api.atlantyda.app',
  timeout: 15000,
});

// Request interceptor
apiClient.interceptors.request.use(async (config) => {
  const token = useAuthStore.getState().token;
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor z refresh token
apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        const refreshToken = useAuthStore.getState().refreshToken;
        const response = await axios.post(
          'https://api.atlantyda.app/auth/refresh',
          { refreshToken }
        );

        const { token, refreshToken: newRefreshToken } = response.data;
        useAuthStore.getState().setTokens(token, newRefreshToken);

        originalRequest.headers.Authorization = `Bearer ${token}`;
        return apiClient(originalRequest);
      } catch (refreshError) {
        useAuthStore.getState().logout();
        return Promise.reject(refreshError);
      }
    }

    return Promise.reject(error);
  }
);

export default apiClient;

Auth Store z Zustand + Persist

// stores/authStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface User {
  id: string;
  email: string;
  name: string;
}

interface AuthStore {
  user: User | null;
  token: string | null;
  refreshToken: string | null;
  isAuthenticated: boolean;
  setUser: (user: User) => void;
  setTokens: (token: string, refreshToken: string) => void;
  logout: () => void;
}

export const useAuthStore = create<AuthStore>()(
  persist(
    (set) => ({
      user: null,
      token: null,
      refreshToken: null,
      isAuthenticated: false,

      setUser: (user) => set({ user, isAuthenticated: true }),

      setTokens: (token, refreshToken) => set({ token, refreshToken }),

      logout: () =>
        set({
          user: null,
          token: null,
          refreshToken: null,
          isAuthenticated: false,
        }),
    }),
    {
      name: 'auth-storage',
      storage: createJSONStorage(() => AsyncStorage),
    }
  )
);

Custom Hooks z React Query

// hooks/useTreasures.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { treasureService, Treasure } from '@/api/treasureService';

export function useTreasures() {
  return useQuery({
    queryKey: ['treasures'],
    queryFn: treasureService.getAll,
  });
}

export function useTreasure(id: string) {
  return useQuery({
    queryKey: ['treasure', id],
    queryFn: () => treasureService.getById(id),
    enabled: !!id,
  });
}

export function useCreateTreasure() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: treasureService.create,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['treasures'] });
    },
  });
}

export function useDeleteTreasure() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: treasureService.delete,
    onMutate: async (id) => {
      await queryClient.cancelQueries({ queryKey: ['treasures'] });
      const previous = queryClient.getQueryData(['treasures']);
      queryClient.setQueryData(['treasures'], (old: Treasure[] | undefined) =>
        old?.filter((t) => t.id !== id)
      );
      return { previous };
    },
    onError: (err, id, context) => {
      queryClient.setQueryData(['treasures'], context?.previous);
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['treasures'] });
    },
  });
}

Ekran z użyciem wszystkich technik

// screens/TreasureListScreen.tsx
import { useTreasures, useDeleteTreasure } from '@/hooks/useTreasures';
import { useAuthStore } from '@/stores/authStore';

export function TreasureListScreen() {
  const { user } = useAuthStore();
  const { data: treasures, isLoading, error, refetch, isRefetching } = useTreasures();
  const deleteMutation = useDeleteTreasure();

  const handleDelete = (id: string) => {
    Alert.alert(
      'Potwierdź',
      'Czy na pewno chcesz usunąć ten skarb?',
      [
        { text: 'Anuluj', style: 'cancel' },
        {
          text: 'Usuń',
          style: 'destructive',
          onPress: () => deleteMutation.mutate(id),
        },
      ]
    );
  };

  if (isLoading) {
    return <LoadingSpinner />;
  }

  if (error) {
    return (
      <ErrorView
        message={error.message}
        onRetry={refetch}
      />
    );
  }

  return (
    <View style={styles.container}>
      <Text style={styles.welcome}>Witaj, {user?.name}!</Text>

      <FlatList
        data={treasures}
        refreshing={isRefetching}
        onRefresh={refetch}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <TreasureCard
            treasure={item}
            onDelete={() => handleDelete(item.id)}
          />
        )}
        ListEmptyComponent={
          <Text style={styles.empty}>Brak skarbów w kolekcji</Text>
        }
      />
    </View>
  );
}

Ten projekt łączy Zustand dla stanu lokalnego (auth, settings), React Query dla stanu serwera i Axios dla komunikacji z API!

Kod do tej lekcji: store.ts
import { create } from 'zustand';

interface AuthStore {
  user: string | null;
  token: string | null;
  // TODO: dodaj akcję login(user, token), która ustawia oba pola
  login: (user: string, token: string) => void;
  logout: () => void;
}

export const useAuthStore = create<AuthStore>((set) => ({
  user: null,
  token: null,
  login: (user, token) => set({ user, token }),
  logout: () => set({ user: null, token: null }),
}));

Hasła z dokumentacji w tej lekcji

Widzisz błąd w tej lekcji?

Zadania praktyczne w grze

  • Edytor kodu

    Uzupełnij kod integrujący wszystkie techniki.

Przydatne artykuły