1.0.7 • Published 2 years ago

@badm/react-store v1.0.7

Weekly downloads
-
License
ISC
Repository
-
Last release
2 years ago

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;
}
1.0.7

2 years ago

1.0.6

2 years ago

1.0.5

2 years ago

1.0.4

2 years ago

1.0.3

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago