264 lines
6.1 KiB
Markdown
264 lines
6.1 KiB
Markdown
# Redux Toolkit Setup
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
yarn add @reduxjs/toolkit react-redux redux-persist
|
|
yarn add -D @types/react-redux
|
|
```
|
|
|
|
## Store Configuration
|
|
|
|
```typescript
|
|
// src/redux/store/index.ts
|
|
import {configureStore} from '@reduxjs/toolkit';
|
|
import {persistStore, persistReducer} from 'redux-persist';
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import {combineReducers} from '@reduxjs/toolkit';
|
|
import authSlice from '../slices/authSlice';
|
|
import userSlice from '../slices/userSlice';
|
|
|
|
const persistConfig = {
|
|
key: 'root',
|
|
storage: AsyncStorage,
|
|
whitelist: ['auth'],
|
|
};
|
|
|
|
const rootReducer = combineReducers({
|
|
auth: authSlice,
|
|
user: userSlice,
|
|
});
|
|
|
|
const persistedReducer = persistReducer(persistConfig, rootReducer);
|
|
|
|
export const store = configureStore({
|
|
reducer: persistedReducer,
|
|
middleware: getDefaultMiddleware =>
|
|
getDefaultMiddleware({
|
|
serializableCheck: {
|
|
ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'],
|
|
},
|
|
}),
|
|
});
|
|
|
|
export const persistor = persistStore(store);
|
|
export type RootState = ReturnType<typeof store.getState>;
|
|
export type AppDispatch = typeof store.dispatch;
|
|
```
|
|
|
|
## Auth Slice
|
|
|
|
```typescript
|
|
// src/redux/slices/authSlice.ts
|
|
import {createSlice, PayloadAction} from '@reduxjs/toolkit';
|
|
|
|
interface User {
|
|
id: string;
|
|
email: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
avatar?: string;
|
|
}
|
|
|
|
interface AuthState {
|
|
isAuthenticated: boolean;
|
|
user: User | null;
|
|
token: string | null;
|
|
refreshToken: string | null;
|
|
loading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
const initialState: AuthState = {
|
|
isAuthenticated: false,
|
|
user: null,
|
|
token: null,
|
|
refreshToken: null,
|
|
loading: false,
|
|
error: null,
|
|
};
|
|
|
|
const authSlice = createSlice({
|
|
name: 'auth',
|
|
initialState,
|
|
reducers: {
|
|
loginStart: (state) => {
|
|
state.loading = true;
|
|
state.error = null;
|
|
},
|
|
loginSuccess: (state, action: PayloadAction<{user: User; token: string; refreshToken: string}>) => {
|
|
state.loading = false;
|
|
state.isAuthenticated = true;
|
|
state.user = action.payload.user;
|
|
state.token = action.payload.token;
|
|
state.refreshToken = action.payload.refreshToken;
|
|
state.error = null;
|
|
},
|
|
loginFailure: (state, action: PayloadAction<string>) => {
|
|
state.loading = false;
|
|
state.isAuthenticated = false;
|
|
state.user = null;
|
|
state.token = null;
|
|
state.refreshToken = null;
|
|
state.error = action.payload;
|
|
},
|
|
logout: (state) => {
|
|
state.isAuthenticated = false;
|
|
state.user = null;
|
|
state.token = null;
|
|
state.refreshToken = null;
|
|
state.error = null;
|
|
},
|
|
updateUser: (state, action: PayloadAction<Partial<User>>) => {
|
|
if (state.user) {
|
|
state.user = {...state.user, ...action.payload};
|
|
}
|
|
},
|
|
clearError: (state) => {
|
|
state.error = null;
|
|
},
|
|
},
|
|
});
|
|
|
|
export const {
|
|
loginStart,
|
|
loginSuccess,
|
|
loginFailure,
|
|
logout,
|
|
updateUser,
|
|
clearError,
|
|
} = authSlice.actions;
|
|
|
|
export default authSlice.reducer;
|
|
```
|
|
|
|
## Typed Hooks
|
|
|
|
```typescript
|
|
// src/redux/hooks.ts
|
|
import {useDispatch, useSelector, TypedUseSelectorHook} from 'react-redux';
|
|
import type {RootState, AppDispatch} from './store';
|
|
|
|
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
|
```
|
|
|
|
## Async Thunks
|
|
|
|
```typescript
|
|
// src/redux/thunks/authThunks.ts
|
|
import {createAsyncThunk} from '@reduxjs/toolkit';
|
|
import {authAPI} from '@services/authAPI';
|
|
|
|
interface LoginCredentials {
|
|
email: string;
|
|
password: string;
|
|
}
|
|
|
|
export const loginUser = createAsyncThunk(
|
|
'auth/loginUser',
|
|
async (credentials: LoginCredentials, {rejectWithValue}) => {
|
|
try {
|
|
const response = await authAPI.login(credentials);
|
|
return response.data;
|
|
} catch (error: any) {
|
|
return rejectWithValue(error.response?.data?.message || 'Login failed');
|
|
}
|
|
}
|
|
);
|
|
|
|
export const refreshToken = createAsyncThunk(
|
|
'auth/refreshToken',
|
|
async (_, {getState, rejectWithValue}) => {
|
|
try {
|
|
const state = getState() as any;
|
|
const refreshToken = state.auth.refreshToken;
|
|
|
|
if (!refreshToken) {
|
|
throw new Error('No refresh token available');
|
|
}
|
|
|
|
const response = await authAPI.refreshToken(refreshToken);
|
|
return response.data;
|
|
} catch (error: any) {
|
|
return rejectWithValue(error.response?.data?.message || 'Token refresh failed');
|
|
}
|
|
}
|
|
);
|
|
```
|
|
|
|
## Provider Setup
|
|
|
|
```typescript
|
|
// App.tsx
|
|
import React from 'react';
|
|
import {Provider} from 'react-redux';
|
|
import {PersistGate} from 'redux-persist/integration/react';
|
|
import {store, persistor} from './src/redux/store';
|
|
import {AppNavigator} from './src/navigation/AppNavigator';
|
|
import {LoadingScreen} from './src/components/common/LoadingScreen';
|
|
|
|
const App: React.FC = () => {
|
|
return (
|
|
<Provider store={store}>
|
|
<PersistGate loading={<LoadingScreen />} persistor={persistor}>
|
|
<AppNavigator />
|
|
</PersistGate>
|
|
</Provider>
|
|
);
|
|
};
|
|
|
|
export default App;
|
|
```
|
|
|
|
## Middleware Configuration
|
|
|
|
```typescript
|
|
// src/redux/middleware/authMiddleware.ts
|
|
import {Middleware} from '@reduxjs/toolkit';
|
|
import {logout} from '../slices/authSlice';
|
|
|
|
export const authMiddleware: Middleware = (store) => (next) => (action) => {
|
|
// Handle token expiration
|
|
if (action.type.endsWith('/rejected') && action.payload?.status === 401) {
|
|
store.dispatch(logout());
|
|
}
|
|
|
|
return next(action);
|
|
};
|
|
```
|
|
|
|
## Selectors
|
|
|
|
```typescript
|
|
// src/redux/selectors/authSelectors.ts
|
|
import {createSelector} from '@reduxjs/toolkit';
|
|
import {RootState} from '../store';
|
|
|
|
export const selectAuth = (state: RootState) => state.auth;
|
|
|
|
export const selectIsAuthenticated = createSelector(
|
|
[selectAuth],
|
|
(auth) => auth.isAuthenticated
|
|
);
|
|
|
|
export const selectUser = createSelector(
|
|
[selectAuth],
|
|
(auth) => auth.user
|
|
);
|
|
|
|
export const selectAuthLoading = createSelector(
|
|
[selectAuth],
|
|
(auth) => auth.loading
|
|
);
|
|
|
|
export const selectAuthError = createSelector(
|
|
[selectAuth],
|
|
(auth) => auth.error
|
|
);
|
|
|
|
export const selectUserFullName = createSelector(
|
|
[selectUser],
|
|
(user) => user ? `${user.firstName} ${user.lastName}` : ''
|
|
);
|
|
``` |