Как применить принцип инверсии зависимостей во фронтенде: абстракция над API-клиентом и хранилищем?
SOLID во фронтенде
DIP во фронтенде: компоненты и хуки зависят от интерфейса вроде `UserApi` или `KeyValueStorage`, а конкретный fetch, axios или localStorage подставляется в корне приложения — через контекст или параметры.
Без DIP
В компоненте axios.get('/api/users') и localStorage.getItem('token'): для теста нужен jest.mock по пути модуля, при SSR падает localStorage is not defined, переезд на GraphQL затрагивает десятки файлов.
С DIP
/** @typedef {{ getUser(id: string): Promise<{ id: string, name: string }> }} UserApi */
const ApiContext = createContext(null);
/** @returns {UserApi} */
function useUserApi() {
const api = useContext(ApiContext);
if (!api) throw new Error('ApiContext не задан');
return api;
}
/** @type {UserApi} */
const httpUserApi = {
getUser: id => fetch(`/api/users/${id}`).then(r => r.json()),
};
const root = <ApiContext.Provider value={httpUserApi}><App /></ApiContext.Provider>;В тестах и Storybook передают { getUser: async () => fakeUser }.
Что ещё абстрагировать
- Хранилище: реализации на localStorage, in-memory для SSR и тестов, IndexedDB.
- Время, аналитику, фича-флаги, логгер.
Когда лишнее
React Query или SWR уже дают слой кэша, а MSW перехватывает запросы на сетевом уровне — часто этого хватает, и свой контекст ради одного эндпоинта избыточен.
Коротко для собеседования
Компоненты зависят от интерфейса вроде UserApi, а конкретную реализацию на fetch или localStorage подставляют в корне через контекст. Это даёт простые подмены в тестах и Storybook и безопасный SSR, но если хватает React Query и MSW, отдельный слой ради одного эндпоинта не нужен.