При работе с RTK Query одной из наиболее распространённых задач становится интеграция механизма авторизации. Наиболее частый сценарий — хранение access token и refresh token в Redux Store с последующей автоматической подстановкой токена в HTTP-запросы.
RTK Query тесно интегрирован с Redux Toolkit, поэтому доступ к
глобальному состоянию возможен напрямую через API
baseQuery.
Обычно структура состояния авторизации выглядит следующим образом:
{
auth: {
accessToken: 'jwt_access_token',
refreshToken: 'jwt_refresh_token',
user: {
id: 1,
email: 'admin@example.com'
}
}
}
В большинстве приложений:
accessToken используется для доступа к APIrefreshToken применяется для обновления access
tokengetStateДля хранения токенов создаётся отдельный Redux slice.
// features/auth/authSlice.js
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
accessToken: null,
refreshToken: null,
user: null
}
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setCredentials(state, action) {
state.accessToken = action.payload.accessToken
state.refreshToken = action.payload.refreshToken
state.user = action.payload.user
},
logout(state) {
state.accessToken = null
state.refreshToken = null
state.user = null
}
}
})
export const {
setCredentials,
logout
} = authSlice.actions
export default authSlice.reducer
// app/store.js
import { configureStore } from '@reduxjs/toolkit'
import authReducer from '../features/auth/authSlice'
import { api } from '../services/api'
export const store = configureStore({
reducer: {
auth: authReducer,
[api.reducerPath]: api.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware)
})
RTK Query предоставляет функцию prepareHeaders,
позволяющую изменять заголовки перед отправкой запроса.
Именно здесь чаще всего происходит интеграция с Redux Store.
// services/api.js
import {
createApi,
fetchBaseQuery
} from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: 'https://api.example.com',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.accessToken
if (token) {
headers.set('authorization', `Bearer ${token}`)
}
return headers
}
}),
endpoints: () => ({})
})
Функция вызывается перед каждым запросом RTK Query.
Вторым аргументом передаётся специальный объект:
{
getState,
endpoint,
type,
forced
}
Наиболее важным является getState.
Пример:
prepareHeaders: (headers, api) => {
const state = api.getState()
console.log(state)
return headers
}
RTK Query получает актуальное состояние Redux Store непосредственно перед выполнением запроса. Благодаря этому токен всегда остаётся свежим.
Одно из преимуществ Redux-хранилища заключается в том, что RTK Query автоматически начинает использовать новый токен сразу после обновления состояния.
Например:
dispatch(setCredentials({
accessToken: 'new_token',
refreshToken: 'refresh_token'
}))
Следующий запрос RTK Query автоматически отправится с новым access token.
Дополнительной синхронизации не требуется.
Типичный сценарий — получение токенов после логина.
// services/authApi.js
import { api } from './api'
export const authApi = api.injectEndpoints({
endpoints: (builder) => ({
login: builder.mutation({
query: (credentials) => ({
url: '/login',
method: 'POST',
body: credentials
})
})
})
})
export const {
useLoginMutation
} = authApi
// components/Login.jsx
import { useDispatch } from 'react-redux'
import { useLoginMutation } from '../services/authApi'
import { setCredentials } from '../features/auth/authSlice'
export default function Login() {
const dispatch = useDispatch()
const [login] = useLoginMutation()
const handleLogin = async () => {
const response = await login({
email: 'admin@example.com',
password: '123456'
}).unwrap()
dispatch(setCredentials(response))
}
return null
}
Метод unwrap() преобразует результат mutation в обычный
Promise.
Без unwrap:
const result = await login(data)
console.log(result.data)
console.log(result.error)
С unwrap:
try {
const data = await login(data).unwrap()
console.log(data)
} catch (error) {
console.log(error)
}
Такой подход значительно упрощает обработку авторизации.
Многие современные приложения хранят access token исключительно в Redux Store.
Преимущества:
Недостаток:
Часто Redux используется совместно с localStorage.
Сценарий:
setCredentials(state, action) {
state.accessToken = action.payload.accessToken
state.refreshToken = action.payload.refreshToken
localStorage.setItem(
'accessToken',
action.payload.accessToken
)
localStorage.setItem(
'refreshToken',
action.payload.refreshToken
)
}
const initialState = {
accessToken: localStorage.getItem('accessToken'),
refreshToken: localStorage.getItem('refreshToken'),
user: null
}
logout(state) {
state.accessToken = null
state.refreshToken = null
state.user = null
localStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken')
}
Главная проблема localStorage — доступность токенов из JavaScript.
Если приложение уязвимо к XSS:
const token = localStorage.getItem('accessToken')
вредоносный код может украсть access token.
Поэтому многие production-приложения:
Вместо прямого доступа к состоянию часто используются селекторы.
// features/auth/selectors.js
export const selectAccessToken = (state) =>
state.auth.accessToken
export const selectCurrentUser = (state) =>
state.auth.user
import { useSelector } from 'react-redux'
import { selectCurrentUser } from './selectors'
export default function Profile() {
const user = useSelector(selectCurrentUser)
return <div>{user.email}</div>
}
Для автоматического сохранения Redux Store часто используется библиотека Redux Persist.
Пример конфигурации:
import storage from 'redux-persist/lib/storage'
import { persistReducer } from 'redux-persist'
const authPersistConfig = {
key: 'auth',
storage
}
const persistedAuthReducer = persistReducer(
authPersistConfig,
authReducer
)
export const store = configureStore({
reducer: {
auth: persistedAuthReducer,
[api.reducerPath]: api.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false
}).concat(api.middleware)
})
Redux Persist добавляет специальные служебные объекты, нарушающие правило сериализуемости Redux Toolkit.
Без отключения проверки консоль будет заполнена предупреждениями.
После выхода пользователя желательно очищать кэш RTK Query.
import { api } from '../services/api'
import { logout } from './authSlice'
export const logoutUser = () => (dispatch) => {
dispatch(logout())
dispatch(api.util.resetApiState())
}
Метод:
api.util.resetApiState()
полностью очищает:
Это особенно важно в multi-user системах.
Наиболее распространённая структура проекта:
src/
├── app/
│ └── store.js
│
├── features/
│ └── auth/
│ ├── authSlice.js
│ ├── selectors.js
│ └── authThunks.js
│
├── services/
│ ├── api.js
│ └── authApi.js
│
└── components/
Типичная архитектура выглядит следующим образом:
Отвечает за:
Отвечает за:
Отвечает за:
RTK Query не заменяет Redux Store полностью.
RTK Query лучше использовать для:
Redux Store лучше подходит для:
Интеграция RTK Query с Redux позволяет объединить обе модели хранения в единую архитектуру приложения.