← Назад к вопросам

Как сделать hooks-based авторизацию не полагаясь на hook?

2.3 Middle🔥 231 комментариев
#React#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как сделать hooks-based авторизацию не полагаясь на hook?

Вопрос о реализации авторизации без хуков - это вопрос об архитектуре и разделении ответственности. На самом деле, правильно организованная система авторизации должна быть независима от React и хуков, работая на низком уровне.

Правильная архитектура авторизации

// 1. Слой сервиса (не зависит от React)
// src/services/auth.service.ts

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

class AuthService {
  private currentUser: User | null = null;
  private token: string | null = null;
  private listeners: Set<(user: User | null) => void> = new Set();

  // Отправляем токен при инициализации
  async initialize() {
    const token = localStorage.getItem("authToken");
    if (token) {
      try {
        const response = await fetch("/api/auth/me", {
          headers: { Authorization: `Bearer ${token}` }
        });
        if (response.ok) {
          this.currentUser = await response.json();
          this.token = token;
          this.notifyListeners();
        } else {
          this.logout();
        }
      } catch (error) {
        this.logout();
      }
    }
  }

  async login(email: string, password: string): Promise<User> {
    const response = await fetch("/api/auth/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email, password })
    });

    if (!response.ok) {
      throw new Error("Login failed");
    }

    const { user, token } = await response.json();
    this.currentUser = user;
    this.token = token;
    localStorage.setItem("authToken", token);
    this.notifyListeners();
    return user;
  }

  async logout() {
    this.currentUser = null;
    this.token = null;
    localStorage.removeItem("authToken");
    this.notifyListeners();
  }

  getUser(): User | null {
    return this.currentUser;
  }

  getToken(): string | null {
    return this.token;
  }

  isAuthenticated(): boolean {
    return this.currentUser !== null;
  }

  hasRole(role: string): boolean {
    return this.currentUser?.role === role;
  }

  // Подписка на изменения авторизации
  subscribe(listener: (user: User | null) => void) {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }

  private notifyListeners() {
    this.listeners.forEach(listener => listener(this.currentUser));
  }
}

export const authService = new AuthService();

Подготовка токена в HTTP запросах

// src/lib/api.ts - вспомогательная функция
import { authService } from "@/services/auth.service";

class ApiClient {
  async fetch(url: string, options: RequestInit = {}) {
    const headers = {
      ...options.headers
    } as HeadersInit;

    const token = authService.getToken();
    if (token) {
      headers["Authorization"] = `Bearer ${token}`;
    }

    const response = await fetch(url, { ...options, headers });

    // Если 401 - токен истёк
    if (response.status === 401) {
      authService.logout();
      window.location.href = "/login";
    }

    return response;
  }
}

export const api = new ApiClient();

React Context для подключения к сервису

// src/contexts/auth.context.tsx
import React, { useEffect, useState } from "react";
import { authService } from "@/services/auth.service";

interface AuthContextType {
  user: User | null;
  isLoading: boolean;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => Promise<void>;
}

const AuthContext = React.createContext<AuthContextType>({
  user: null,
  isLoading: true,
  isAuthenticated: false,
  login: async () => {},
  logout: async () => {}
});

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  // Инициализируем сервис при mount
  useEffect(() => {
    const initAuth = async () => {
      await authService.initialize();
      setUser(authService.getUser());
      setIsLoading(false);
    };

    initAuth();
  }, []);

  // Подписываемся на изменения авторизации
  useEffect(() => {
    const unsubscribe = authService.subscribe((updatedUser) => {
      setUser(updatedUser);
    });

    return unsubscribe;
  }, []);

  const value: AuthContextType = {
    user,
    isLoading,
    isAuthenticated: user !== null,
    login: async (email, password) => {
      await authService.login(email, password);
    },
    logout: async () => {
      await authService.logout();
    }
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

// Хук для доступа к контексту
export function useAuth() {
  const context = React.useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth должен использоваться в AuthProvider");
  }
  return context;
}

Использование без хуков - в компонентах

// Класс компонент тоже работает
class Dashboard extends React.Component {
  state = {
    user: null,
    loading: true
  };

  componentDidMount() {
    // Получаем данные из сервиса (не зависит от хуков)
    this.setState({
      user: authService.getUser(),
      loading: false
    });

    // Подписываемся на изменения
    this.unsubscribe = authService.subscribe((user) => {
      this.setState({ user });
    });
  }

  componentWillUnmount() {
    this.unsubscribe?.();
  }

  render() {
    const { user, loading } = this.state;

    if (loading) return <div>Loading...</div>;
    if (!user) return <div>Not authenticated</div>;

    return <div>Welcome, {user.name}</div>;
  }
}

Защита роутов без React Router хуков

// src/guards/auth.guard.tsx
import { ReactNode } from "react";
import { authService } from "@/services/auth.service";
import { useAuth } from "@/contexts/auth.context";

interface ProtectedRouteProps {
  children: ReactNode;
  requiredRole?: string;
  fallback?: ReactNode;
}

export function ProtectedRoute({
  children,
  requiredRole,
  fallback = <div>Access Denied</div>
}: ProtectedRouteProps) {
  const { user, isLoading } = useAuth();

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (!user) {
    return <div>Please login</div>;
  }

  if (requiredRole && !authService.hasRole(requiredRole)) {
    return fallback;
  }

  return <>{children}</>;
}

Работа с авторизацией без хуков - в утилитах

// src/lib/auth-utils.ts
import { authService } from "@/services/auth.service";

// Проверка авторизации без React
export function isUserLoggedIn(): boolean {
  return authService.isAuthenticated();
}

// Получение текущего пользователя
export function getCurrentUser() {
  return authService.getUser();
}

// Получение токена для запросов
export function getAuthToken(): string | null {
  return authService.getToken();
}

// Проверка прав
export function hasPermission(role: string): boolean {
  return authService.hasRole(role);
}

// Использование в любом месте приложения
if (isUserLoggedIn()) {
  console.log("User:", getCurrentUser());
}

Интеграция с middleware и interceptors

// src/lib/api-interceptor.ts
import { authService } from "@/services/auth.service";

// Переопределяем fetch глобально
const originalFetch = window.fetch;

window.fetch = async (input: RequestInfo, init?: RequestInit) => {
  const headers = {
    ...init?.headers
  } as HeadersInit;

  // Добавляем токен в каждый запрос
  const token = authService.getToken();
  if (token) {
    headers["Authorization"] = `Bearer ${token}`;
  }

  let response = await originalFetch(input, { ...init, headers });

  // Если 401 - пытаемся обновить токен
  if (response.status === 401) {
    await authService.logout();
    // Или пытаемся refresh token
    // const refreshed = await authService.refreshToken();
    // if (refreshed) {
    //   return window.fetch(input, init); // повторный запрос
    // }
  }

  return response;
};

Next.js специфичный подход

// src/lib/auth.ts - для использования везде
import { cookies } from "next/headers";

export async function getAuthToken() {
  const cookieStore = await cookies();
  return cookieStore.get("authToken")?.value || null;
}

export async function getCurrentUser() {
  const token = await getAuthToken();
  if (!token) return null;

  try {
    const response = await fetch("http://localhost:3000/api/auth/me", {
      headers: { Authorization: `Bearer ${token}` }
    });
    if (response.ok) {
      return await response.json();
    }
  } catch (error) {
    console.error("Failed to get user", error);
  }
  return null;
}

// Использование в server components
export default async function Dashboard() {
  const user = await getCurrentUser();

  if (!user) {
    return <div>Not authenticated</div>;
  }

  return <div>Welcome, {user.name}</div>;
}

Итоговая архитектура

src/
  services/
    auth.service.ts       <- Core авторизация (NO React)
  lib/
    api.ts                <- HTTP client с авторизацией
    auth-utils.ts         <- Утилиты для доступа к auth
  contexts/
    auth.context.tsx      <- React Context обёртка
  hooks/
    useAuth.ts            <- Хук для доступа к контексту
  guards/
    ProtectedRoute.tsx    <- Компоненты для защиты
  components/
    Header.tsx            <- Компоненты используют хуки

Главный принцип

Авторизация - это слой который должен быть независим от React. React должен подписываться на изменения в сервисе авторизации, а не быть его основой. Таким образом вы можете использовать авторизацию везде:

  • В React компонентах (с хуками)
  • В класс компонентах (без хуков)
  • В утилитах и вспомогательных функциях
  • В server components (Next.js)
  • Даже в обычном JavaScript коде

Это правильное разделение ответственности и делает код более переиспользуемым и тестируемым.

Как сделать hooks-based авторизацию не полагаясь на hook? | PrepBro