Kurs React Native · Moduł 4: Stan i dane z API
Projekt: Skarbiec Atlantydy - Kompletna aplikacja
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.tsxAPI 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.