Skip to content

Ann2827/request-store-manager

Repository files navigation

request-store-manager

NPM JavaScript Style Guide License

Позволяет писать меньше шаблонного кода при запросах к api.

Table of Contents

Motivation

При создании нового фронтового приложения каждый раз приходится организовывать работу связки:

  • Запрос/отправка данных по api
  • Запускаем loader
  • Получаем ответ. Валидируем полученные данные.
  • Останавливаем loader
  • Показываем сообщение (успех/ошибка)
  • Приводим полученные данные в нужный формат и сохраняем в хранилище
  • Кэшируем.

Часто разработка фронта и бэка происходит в асинхронном формате. Фронтовое приложение хочется иметь возможность разрабатывать/запускать не зависимо от серверной части. Но добавление моков на все приложение занимает не мало времени.

Так же, данные запрашиваются из разных мест. А хотелось бы собрать все запросы в одном файле. И при вызове запроса писать как можно меньше однообразного кода.

Данная библиотека позволяет:

  • описать полный конфиг запросов в одном месте
  • подключить слушатели хранилища к своим компонентам: Loader, Notifications
  • вызывать запросы в пару строк
  • запускать mock mode, кэшировать данные, обращаться к хранилищу. Есть возможность подключить сообщения к i18 и описать стандартные ошибки

Features

  • Loader - нужно подключить слушатель к своему компоненту Loader. При запросах будет запускаться автоматически (можно запретить автозапуск для отдельных запросов). Так же можно запускать вручную.
  • Messages - напишите сообщения для стандартных ошибок (404, 500,..). При использовании i18, вместо текстов положите ключи.
  • Notifications - нужно подключить слушатель к своему компоненту Notifications. При запросах будет вызывать сообщения из MessagesStore (можно запретить автозапуск для отдельных запросов). Так же можно вызывать кастомные сообщения вручную.
  • Https - выполняет запросы из конфига (можно выполнять неописанные запросы). Чаще используется для POST, PATCH, DELETE, PUT.
  • Needs - минималистичный запуск GET запросов с сохранением данных в хранилище и кэшированием.
  • Cache - работает с localStorage, sessionStorage.
  • CacheStrict - обрабатывает только значения по заданным ключам.
  • Timer - просто таймер.
  • Request - выполняет fetch запросы и mock запросы.
  • Token - хранилище токенов.
  • Store - хранилище ответов из Needs. Можно создавать самостоятельные хранилища.

✨ Demo

Example admin

Getting Started

npm install --save request-store-manager

🚀 Usage

Settings config

  1. Создайте папку src/api
  • src/api
    • index.ts
    • mocks.ts
    • types.ts
    • urls.ts
  1. Опишите типы api/types.ts
import type { RequestManagerBase, IHttpsRequest, TNotificationsBase } from 'request-store-manager';

type TError = { message?: string[]; error?: string; statusCode?: number };

interface ITask {
  id: number;
  title: string;
}

/**
  * Задайте имя токена. В запросах вы будете указывать это имя. Если вы работаете с несколькими api,
  * то можно задать несколько имен.
**/ 
type TTokens = 'main' | 'second' | 'third';

/**
 * Задайте формат хранилища
**/ 
type TStore = {
  tasks: { backlog: string[]; done: string[] };
  zero: boolean;
  non: null;
}

/**
  * Опишите запросы (для GET можно добавить storeKey для автосохранения), типы успешных ответов
**/ 
interface RM extends RequestManagerBase<TTokens, TStore> {
  getTasks: {
    fn: (quantity: number) => IHttpsRequest<TTokens>;
    success: { data: { type: 'backlog' | 'done'; text: string }[]; quantity: number };
    storeKey: 'tasks';
    error: TError;
  };
  getZero: {
    fn: () => IHttpsRequest<TTokens>;
    success: boolean;
    storeKey: 'zero';
  };
  postAuth: {
    fn: () => IHttpsRequest<TTokens>;
    success: boolean;
  };
}

/**
  * Дополнительно
  * =================================================
  * Вы можете расширить передаваемые поля уведомления. По умолчанию Partial<Record<'title' | 'text' | 'action', string>>
**/ 
interface TNotifications extends TNotificationsBase {
  action2: string;
}
  1. Создайте конфиг и подключите его index.tsx
import './api';

api/index.ts

import { HttpsStore, ICustomFetchCheckProps, NeedsStore, NotificationsStore, SettingsStore } from 'request-store-manager';

import { GET_TASKS, POST_AUTH, POST_TASK } from './urls';
import { mockPosts, mockSuccessAnswer, mockTasks, mockUsers } from './mocks';

export * from './types';

function validationSuccessAnswer(dataJson: unknown, response: Response | undefined): dataJson is TAnswer<unknown> {
  return !!response?.ok && IsObject(dataJson);
}

requestManager = new RequestManager<TTokens, TStore, RM>({
      settings: {
        logger: false,
        notifications: {},
        cache: { prefix: 'test' },
        request: { mockMode: true },
        https: {
          waitToken: false,
          notifications: false,
          loader: false,
        },
      },
      tokens: {
        main: {
          template: 'bearer',
          cache: {
            maxAge: 60 * 24,
          },
        },
      },
      namedRequests: {
        getTasks: {
          request: (quantity: number) => ({
            url: 'https://test.com/' + quantity,
            method: 'GET',
            tokenName: 'main',
          }),
          validation: (dataJson, response): dataJson is RM['getTasks']['success'] =>
            !!response?.ok && typeof dataJson === 'object',
          mock: (input, init) => {
            const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
            const quantity = Number(url.split('/').reverse()[0]);
            return new Response(
              JSON.stringify({
                data: [
                  { type: 'backlog', text: 'task1' },
                  { type: 'done', text: 'tsak2' },
                ],
                quantity,
              }),
              {
                status: 200,
                statusText: 'OK',
              },
            );
          },
          store: {
            key: 'tasks',
            default: { backlog: [], done: [] },
            converter: ({ state, validData }) => {
              const { backlog, done } = Object.groupBy(validData.data, ({ type }) => type);
              return { backlog: backlog?.map(({ text }) => text) || [], done: done?.map(({ text }) => text) || [] };
            },
            validation: (data): data is Store[RM['getTasks']['storeKey']] =>
              !!data && typeof data === 'object' && 'backlog' in data && 'done' in data,
            cache: { maxAge: 0, place: 'sessionStorage' },
            empty: (value) => value.backlog.length === 0 && value.done.length === 0,
          },
          afterRequest: ({ response, input }) => {
            if (!response.ok) return;
            requestManager
              .getModule('notifications')
              .send({ data: { text: 'Данные успешно получены.' }, type: 'success' });
          },
        },
        getZero: {
          request: () => ({
            url: 'https://test.com/',
            method: 'GET',
            tokenName: 'main',
          }),
          store: {
            key: 'zero',
            default: false,
          },
        },
        postAuth: () => ({
          url: 'https://test.com/',
          method: 'GET',
          tokenName: 'main',
        }),
      },
      messages: {
        codes: {
          403: {
            title: 'errors.error403',
          },
          default: {
            title: 'errors.errorTitle',
          },
        },
      },
});

Connect Loader component

App.tsx

import * as React from 'react';
import { useRoutes } from 'react-router-dom';

import { routes } from 'src/navigation/routes';
import { Loader, Notifications } from 'src/modules';

export const App: React.FC = () => {
  const page = useRoutes(routes);
  return (
    <div>
      <Loader />
      <Notifications />
      {page}
    </div>
  );
};

Loader.tsx

import * as React from 'react';
import requestManager from '../api';

import { LoaderComponent, LoaderComponentProps } from 'src/components';

export const Loader: React.FC<LoaderComponentProps> = (props) => {
  const { active } = React.useSyncExternalStore(requestManager.connectLoader.subscribe, requestManager.connectLoader.state); // react v >= 18

  if (!active) return null;

  return <LoaderComponent {...props} active={active} />;
};

Connect Notifications component

App.tsx

import * as React from 'react';
import { useRoutes } from 'react-router-dom';

import { routes } from 'src/navigation/routes';
import { Loader, Notifications } from 'src/modules';

export const App: React.FC = () => {
  const page = useRoutes(routes);
  return (
    <div>
      <Loader />
      <Notifications />
      {page}
    </div>
  );
};

Notifications.tsx

import * as React from 'react';
import requestManager from '../api';
import { useTranslation } from 'react-i18next';
import { Alert, AlertTitle } from '@mui/material';

// For test notification view
// requestManager.sendNotification({ data: { title: 'My title', text: 'Descr' } });

export const Notifications: React.FC = () => {
  const notifications = React.useSyncExternalStore(requestManager.connectNotifications.subscribe, requestManager.connectNotifications.state); // react v >= 18
  const { t } = useTranslation();

  return (
    <div>
      {notifications.map(({ id, type, data, response, drop }) => (
        <Alert
          key={id}
          severity={type}
          onClose={() => {
            drop(id);
          }}
        >
          <AlertTitle>{t(data?.title || '', { errorCode: response?.status || '' })}</AlertTitle>
          {data?.text ? t(data.text) : null}
        </Alert>
      ))}
    </div>
  );
};

Use

auth.hook.ts

import requestManager from '../api';
import * as React from 'react';
import { useNavigate } from 'react-router-dom';

export const useAuth = () => {
  const navigate = useNavigate();

  return {
    login: React.useCallback(
      async (props: { email: string; password: string }) => {
        const { validData } = await requestManager.namedRequest('postAuth', props);
        if (validData) {
          requestManager.setToken('main', validData.token);
          navigate('/dashboard');
        }
      },
      [navigate],
    ),
    logout: React.useCallback(() => {
      requestManager.restart();
      navigate('/');
    }, [navigate]),
  };
};

LoginPage.tsx

import * as React from 'react';
import { useAuth } from 'src/hooks';

export const LoginPage: React.FC = () => {
  const { login } = useAuth();

  return (
    <div>
      <h1>Login Page</h1>
      <button onClick={() => { login({ email, password }) }}>Login</button>
    </div>
  );
};

TasksPage.tsx

import { HttpsStore, ScenariosStore, useNeeds } from 'library-react-hooks';
import * as React from 'react';

import { ITask } from 'src/types';

export const TasksPage: React.FC = () => {
  const { tasks } = React.useSyncExternalStore(requestManager.subscribe, requestManager.state); // react v >= 18
  React.useEffect(() => {
    requestManager.needAction('tasks', NeedsActionTypes.request, 1);
  }, []);
  const { store } = useNeeds(['tasks']); // GET укажите какие данные нужно подгрузить на этой странице

  const onAdd = React.useCallback(async (task: Omit<ITask, 'id'>) => {
    await requestManager.namedRequest('postTask', task); // POST, PUT, PATCH
    // ответ можно обработать тут или в afterRequest
  }, []);

  const freeRequest = async () => {
    const { dataJson, response } = await requestManager.getModule('request').fetch('https://test.com/3');
    if (response?.ok) {
      // do something
    } else {}
  };

  return (
    <div>
      <h1>Tasks Page</h1>
      <ul>
        {store?.tasks?.map(({ id, title }) => (
          <div key={id}>{title}</div>
        ))}
      </ul>
      <button onClick={() => { onAdd({ title: 'new task' }) }}>Add task</button>
    </div>
  );
};

🤝 Contributing

Contributions, issues and feature requests are welcome. Check the contributing guide.

📝 License

Copyright © 2025 Bystrova Ann.
This project is MIT licensed.

Contact

Bystrova Ann - ann.bystrova96@mail.ru

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •