1.0.7 • Published 2 years ago
@badm/react-store v1.0.7
BaDM React Store (WIP)
Эта библиотека упрощает построение хранилищ данных на основе MobX.
Основная задача - упростить взаимодействие между сетевыми запросами, хранилищем, и React компонентами. Хранилище может быть представлено любым объектом. Библиотека позволяет создавать вокруг него запросы. Архитектурно хранилище знает о существование запросов (как сущности), но ничего не знает о том, как они устроены.
Решает такие проблемы:
- позволяет вынести запросы к сетевым ресурсам из компонентов React
- благодаря TypeScript, позволяет полностью типизировать все структуры запросов и ответов сервера
- абстрагируется от того, как и с помощью чего происходит обмен данными с сервером (можно использовать различные подходы (REST API, GraphQL) и протоколы (HTTP, WebSocket))
Примеры
Создание хранилища:
import { createStore } from "@badm/react-store"
import { observable } from "mobx"
const authStore = observable({
token: "",
loggedIn: false
})
export const store = createStore({
authStore
})
store.setDefaultErrorsHandler((store, errors, request) => {
// обработка ошибок возникающих при запросах
})
Встроенная библиотека для осуществления HTTP запросов
import http from "@badm/react-store/lib/http"
http.setBaseUrl("https://api.host.com")
// Перед выполнением любой запрос можно модифицировать.
// Те же параметры доступны для каждого запроса по отдельности.
http.requestInterceptor(options => {
options.useBearerToken("...")
options.useResponseFormat('json') // json | text | blob
options.useBodyFormatter(request => JSON.stringify(request)) // default value
options.useHeaders({
"Content-Type": "application/json",
})
// можно добавить обработчики для конкретных статусов
options.onStatus([401, 403], context => {
context.setResult({
errors: [
{unauthorized: true}
]
})
context.stopPipe() // останавливает выполнение цепочки (не будет вызван onResponseComplete)
})
// Необходим для того, чтобы дать понять библиотеке успешно выполнился запрос или нет.
// setResult принимает объект вида { data, errors }
// где оба свойства могут принимать любые значения.
// Если будет передано свойство errors, мутация хранилища не будет вызываться.
// Также, будет вызван зарегестрированный обработчик ошибок (в setDefaultErrorsHandler)
options.onResponseComplete((responseResult, context) => {
if (responseResult.error) {
context.setResult({
errors: [{message: 'Request error!'}]
})
return
}
context.setResult(responseResult)
})
// Если возникла проблема при подключение к хосту или, к примеру, не удалось распарсить json.
// Можно использовать для более точечного логирования, или переопределить ошибку, которая
// будет направлена в обработчик установленный setDefaultErrorsHandler-ом.
options.onError((type, exception, context) => {
if (type === 'network') {
// some logs, or custom result (context.setErrors())
}
})
})
Все доступные методы
Настройки для всех запросов:
http.requestInterceptor(interceptorOptions)
Методы для создания запросов:
http.get(urlOrFunc, interceptorOptions?)
http.post(urlOrFunc, interceptorOptions?)
http.put(urlOrFunc, interceptorOptions?)
http.delete(urlOrFunc, interceptorOptions?)
http.patch(urlOrFunc, interceptorOptions?)
Типы аргументов:
urlOrFunc: string | ({ request, variables }) => string
Примеры создания запросов
type MyRequest = { login: string; password: string }
type MyResponse = { loggedIn: boolean; token: string }
type MyVariables = { rememberMe: boolean }
// Пример создания запроса, который обновляет хранилище в случае успешного выполнения.
export const LoginRequest = store.mutableRequest(
http.post<MyResponse, MyRequest, MyVariables>(
({request, variables}) => `/login?remember=${variables?.rememberMe ? 'true' : 'false'}`),
(store, response, request, variables) => {
store.auth.loggedIn = true
store.auth.token = response.token
})
// 1. Выполнение там, где недоступны React Hooks:
const executeOptions = {variables: {rememberMe: false}}
const {data, errors} = await LoginRequest.getExecutor().execute({login: "admin", password: "123456"}, executeOptions)
// 2. В контексте функционального компонента:
function LoginComponent() {
const [execute, isInFly] = LoginRequest.useLocal() // это хук
const onSubmit = useCallback(() => execute({login: "admin", password: "123456"}, executeOptions), [])
return <Form>...</Form>
}
// ------------------
// Пример создание запроса для загрузки файла
const downloadFileRequest = store.immutableRequest(
http.post<Blob>(`/my-file`,
options => options.useResponseFormat('blob')))
downloadFileRequest.getExecutor().execute().then(result => {
const blob = result.data
// download blob...
})
// ------------------
// Как можно получить объект хранилища в любом контексте.
const MyComponent = observer(() => {
const globalStore = store.get()
if (!globalStore.auth.loggedIn) {
// ...
}
return /*...*/
})
/**
* TRequest, TResponse, TVariables по умолчанию равны any.
* Следует указать свои типы.
*
* Значение requestFn должно удовлетворять типу:
* (request: TRequest, signal: AbortSignal, variables?: TVariables)
* => Promise<{ data?: TResponse, errors?: any }>
*
* Например:
async (req, signal, vars) => {
const response = await fetch("https://google.com/", {signal})
return {data: await response.json()}
}
*/
// Создание запроса через "низкоуровневый" Fluent API
export const MyRequest = store.createRequest()
.fetch(new FetchFunction<TRequest, TResponse, TVariables>(requestFn))
.immutable()
// или
.mutateStore((store, data, request, vars) => {
// store - объект хранилища
// data - результат выполнения запроса
// request - объект запроса
// vars - переменные (опц.)
})
// также, опционально
.registerErrorsHandler((store, errors, request, variables) => /*...*/)
// Также, доступны такие методы:
store.immutableRequest(new FetchFunction<TRequest, TResponse, TVariables>(requestFn))
store.createRequest().mutableRequest(
new FetchFunction<TRequest, TResponse, TVariables>(requestFn),
(store, data, request, vars) => {
// store - объект хранилища
// data - результат выполнения запроса
// request - объект запроса
// vars - переменные (опц.)
}
)
// Библиотека подразумевает, что вызовы подобные
// new FetchFunction<TRequest, TResponse, TVariables>(requestFn)
// Будут помещены в вспомогательные функции.
// Например, как это выглядит со встроенной библиотекой http:
const DeleteUserRequest = store.mutableRequest(
http.delete(({variables: {id}}) => `/users/${id}`),
(store, data, request, {id: userId}) => {
// ...
// remove user from store by userId
}
)
Описание дополнительных параметров.
// При выполнение запроса напрямую
request.getExecutor().execute(request, executeOptions)
// Или с использованием хука
const [execute, isInFly] = loginRequest.useLocal(options)
execute(request, executeOptions)
// ...доступны такие необязательные параметры:
const executeOptions: {
variables: any // объект с кастомными переменными
// Если false, то при запуске нового запроса старый будет отменен в том случае, если он не успел завершится.
// Если true, то новый запрос не бдет запущен, если старый не успел завершится.
// Важно: используется только при работе с запросом через хук.
// при использование getExecutor() ни на что не влияет.
rejectIfExecuting: boolean // по умолчанию false;
// Позволяет установить минимальное количество времени для isInFly
// Фактически, выполняет запрос, и если он выполнился слишком быстро, ждет оставшееся время.
minimumDelay: number // по умолчанию 0;
// Позволяет установить таймаут до того, как запрос будет фактически запущен.
// Если rejectIfExecuting имеет значение false, то данный параметр можно использовать для
// реализации debounce.
delayBeforeSend: number // по умолчанию 0;
}
const options: {
// Управляет начальным состоянием isInFly
initialStateIsFly: boolean // false;
// Если false, то при окончания жизненного цикла компонента (unmount) текущий выполняемый запрос будет отменен.
// Если true, то не отменяет запрос.
disableUnmountAbort: boolean // по умолчанию false;
}