@aic/react-remote-data-provider
v3.0.2
Published
Remote data provider for get/post requests by redux/components
Downloads
34
Readme
react-remote-data-provider
Гибкий react компонент для загрузки данных из API и хранения их в redux
Содержание
- Установка
- Подключение
- Базовый пример
- API
- Как это работает
- Композиция провайдеров
Composer
- Доступ к данным в redux
- Предзагрузка данных
- Расширения для remote-data-provider
Установка
via npm:
npm i @aic/react-remote-data-provider
via yarn:
yarn add @aic/react-remote-data-provider
Подключение
Подключите remote-data-provider редьюсер к redux по ключу remoteData
:
import { combineReducers } from 'redux'
import { reducer as remoteDataReducer } from '@aic/react-remote-data-provider'
const rootReducer = combineReducers({
// ... остальные редьюсеры
remoteData: remoteDataReducer
})
export default rootReducer
Базовый пример
remote-data-provider
можно использовать 3 способами: хук, компонент или HOC:
Хук useRemoteData
import React from 'react'
import { useRemoteData } from '@aic/react-remote-data-provider'
export default function MyGreatComponent (props) {
const {
response, // последний ответ от сервера, если есть, иначе undefined
request, // текущий запрос, аналогичный запросу из параметров
error, // последняя ошибка, если есть - содержит поля `status`, `message` и `data`
isEmpty, // Boolean флаг, указывает, есть ли ответ с сервера
isAjax, // Boolean флаг, указывает, происходит ли загрузка данных
isError, // Boolean флаг, указывает, произошла ли ошибка во время последнего запроса
reload, // функция, при вызове которой происходит перезагрузка данных; возвращает Promise
clear, // функция, при вызове которой очищаются данные из redux
providerId // уникальный id провайдера
} = useRemoteData({
// уникальный ключ, по которому в redux будут лежать данные
reducerKey: 'myGreatData',
// параметры запроса, аналогичные axios
// если какие-либо параметры меняются, происходит автоматическая перезагрузка данных
request: {
url: 'http://my.perfect.api.com/get/some/data/',
params: {
count: props.count
}
}
}) // компонент будет перерендериваться каждый раз, когда меняется состояние загрузки
if (isEmpty) return null
return JSON.stringify(response)
}
Компонент RemoteDataProvider
import React from 'react'
import { RemoteDataProvider } from '@aic/react-remote-data-provider'
export default function MyGreatComponent (props) {
return (
<RemoteDataProvider {...{
// параметры аналогичны `useRemoteData`
reducerKey: 'myGreatData',
request: {
url: 'http://my.perfect.api.com/get/some/data/',
params: {
count: props.count
}
},
// дополнительный параметр для `RemoteDataProvider`
// если `true`, то children функция будет вызываться только тогда,
// когда данные успешно загружены
onlyWithData: true // такая запись не обязательна, т.к. по умолчанию `true`
}}>
{/* children функция, вызывается каждый раз при изменении состояния загрузки */}
{({ // объект аналогичен результату `useRemoteData`
response, request, error, isEmpty, isAjax, isError, reload, clear
}) => {
// если параметр `onlyWithData` не установлен в `false`,
// проверять `isEmpty` нет необходимости
return JSON.stringify(response)
}}
</RemoteDataProvider>
)
}
HOC withRemoteData
import React from 'react'
import { withRemoteData } from '@aic/react-remote-data-provider'
// `withRemoteData` может принимать как функцию, которая принимает `props`
// и возвращает один (или массив, для нескольких запросов) объект с параметрами,
// так и просто объект (или несколько) с параметрами
export default withRemoteData(props => ({
// параметры аналогичны `RemoteDataProvider`
reducerKey: 'myGreatData',
request: {
url: 'http://my.perfect.api.com/get/some/data/',
params: {
count: props.count
}
},
onlyWithData: true, // такая запись не обязательна, по умолчанию `true`
// дополнительный параметр для `withRemoteData`
// определяет ключ в `props`, где будут лежать данные
// если не указан, данные будут объеденены с `props`
stateKey: 'someData'
}))(MyGreatComponent)
function MyGreatComponent (props) {
// данные лежат по ключу `someData`
const { response, request, error, isEmpty, isAjax, isError, reload, clear } = props.someData
// если параметр `onlyWithData` не установлен в `false`,
// проверять `isEmpty` нет необходимости
return JSON.stringify(response)
}
API
Объект данных
Данный объект содержит информацию о текущем состоянии данных и функции для работы с ним. Он возвращается хуком useRemoteData
, передается как первый параметр children функции у компонента RemoteDataProvider
, и передается в props
с помощью HOC withRemoteData
.
response: any
: Ответ сервера, если запрос прошел успешно. Во время первой загрузки равенundefined
. Во время последующих перезагрузок, когда меняетсяrequest
или вызываетсяreload
, предыдущий ответ сохраняется и доступен во время загрузки.request: AxiosConfig
: Параметры запроса. Соответствуют переданным параметрам вremote-data-provider
, по этому редко используются.error?: { status?: number, message?: string, data?: any }
: Объект с ошибкой, если запрос закончился неудачно - при этомresponse
, если он был, очищается. Если ошибки не произошло, равенundefined
. Содержит (по возможности) поляstatus
- статус (код) ответа сервера;message
- сообщение ответа сервера;data
- тело ответа сервера. Как только запрос начинается или заканчивается успешно, ошибка очищается.isEmpty: boolean
: Флаг,true
когдаresponse
пустой - если данные ни разу не были загружены или произошла ошибка.isAjax: boolean
: Флаг,true
когда происходит загрузка или обновление данных.isError: boolean
: Флаг,true
когда произошла ошибка загрузки данных.reload: () => Promise
: Функция, которая перезагружает данные с последнимrequest
. Возвращает Promise загрузки.clear: Function
: Функция, которая очищает данные в redux. Не используйте ее, еслиremote-data-provider
еще смонтирован, иначе загрузка начнется по новой.providerId: string
: Уникальный id для каждого инстансаremote-data-provider
.
Общие параметры
Данные параметры имеют все вариации - хук useRemoteData
, компонент RemoteDataProvider
, HOC withRemoteData
.
request: AxiosRequestConfig
:remote-data-provider
использует axios для выполнения запросов.request
содержит параметры дляaxios
, весь список находится здесь. Любые изменения в данном объекте (при перерисовке) приводят к перезагрузке данных - при этом происходит глубокое сравнение (_.equal
). Не используйте функции вrequest
.requestFunctions?: AxiosRequestConfig
: Аналогичноrequest
, только для функций изaxios
конфига. Изменения в данном объекте не приводят к перезагрузке данных.reducerKey?: string
: Все данные о состоянии и результате загрузки будут храниться в redux по ключуreducerKey
. Обычно, это уникальное значение для каждого компонента. В случае, еслиreducerKey
не указан, то он будет равен строковому значениюrequest
(для запроса из примера примерно"url='http://my.perfect.api.com/get/some/data'¶ms[count]=5"
когдаcount
равен 5).reducerPath?: string | string[]
: ПараметрreducerPath
, как иreducerKey
, определяет место хранения в redux store.reducerPath
определяет вложенность данных. Например, еслиreducerPath = ['some', 'path']
, аreducerKey = 'someKey'
, то данные будут храниться в redux по путиremoteData.some.path.someKey
.axiosInstance?: AxiosInstance
: экземплярaxios
с предустановленными параметрами запроса. Создается с помощьюaxios.create({ ...params })
.disabled?: boolean
: еслиtrue
, то загрузки и обновления данных происходить не будет. Еслиrequest
из запроса и из стора совпадают - то будут возвращены актуальные данные, иначе - пустой объект данных.- Могут быть дополнительные параметры, они все передаются в
middlewares
.
Хук useRemoteData
import { useRemoteData } from '@aic/react-remote-data-provider'
...
const remoteData = useRemoteData(params, requestDeps)
Хук загружает и обновляет данные, перерисовывая компонент при каждом изменении состояния. Возвращает объект данных.
params: object
: Соответствуют общим параметрам.requestDeps?: any[]
: Зависимости, используемые в параметреrequest
, аналогично второму аргументу дляuseMemo
. Например, для запроса из примера
(request = { url: '...', params: { count: props.count } }
) в качествеrequestDeps
нужно передать[props.count]
. Еслиrequest
постоянный (не меняется), нужно передавать пустой массив ([]
). При этом зависимости для остальных параметров, кромеrequest
, передавать не нужно. Используется для улучшения производительности. Если вы не уверены, что нужно передавать какrequestDeps
, не используйте этот параметр.
Компонент RemoteDataProvider
import { RemoteDataProvider } from '@aic/react-remote-data-provider'
...
<RemoteDataProvider { ...params }>
{(remoteData, props) => { ... }}
</RemoteDataProvider>
Компонент является оберткой над хуком useRemoteData
. Вызывает children функцию при каждом изменении состояния данных.
params: object
: В дополнение к общим параметрам может содержать:onlyWithData?: boolean = true
: Еслиtrue
, children функция будет вызываться только тогда, когда есть данные (isEmpty === false
). По умолчаниюtrue
.requestDeps: any[]
: соответствует второму аргументу хукаuseRemoteData
.
children: function (remoteData: object, props: object): ReactNode
: children функция, принимает первым аргументом объект данных, вторым - всеprops
, переданныеRemoteDataProvider
.
По умолчанию, children
функция вызывается только тогда, когда есть загруженные данные (response
). Это удобно, и позволяет с уверенностью использовать response
, однако, уменьшает гибкость. Для большего контроля, можно использовать параметр onlyWithData=false
, при котором RemoteDataProvider будет вызывать children
функцию каждый раз, когда меняется состояние загрузки.
Например:
import React from 'react'
import { RemoteDataProvider } from '@aic/react-remote-data-provider'
export const MyGreatComponent = (props) => (
<RemoteDataProvider {...{
reducerKey: 'myGreatData',
request: {
url: 'http://my.perfect.api.com/get/some/data/',
params: {
count: props.count
}
},
onlyWithData: false // меняем параметр
}}>
{({ isAjax, isEmpty, isError, response, error }) => {
if (isError) return `Упс, произошла ошибка ${error.status}`
if (isEmpty) return `Загрузка...`
let result = JSON.stringify(response)
if (isAjax) result += ' новые данные уже близко...'
return result
}}
</RemoteDataProvider>
)
HOC withRemoteData
import { withRemoteData } from '@aic/react-remote-data-provider'
...
withRemoteData(params, params, ...)(Component)
withRemoteData(props => params)(Component)
withRemoteData(props => [params, params, ...])(Component)
HOC оборачивает компонент, передавая в props
объект(ы) данных. Может принимать:
Просто объекты с параметрами - один или несколько, как аргументы.
Функцию, возвращающую объект с параметрами.
Функцию, возвращающую массив с несколькими объектами с параметрами.
params: object
: В дополнение к общим параметрам может содержать:onlyWithData?: boolean = true
: Еслиtrue
, children функция будет вызываться только тогда, когда есть данные (isEmpty === false
). По умолчаниюtrue
.requestDeps: any[]
: соответствует второму аргументу хукаuseRemoteData
.stateKey?: string | string[]
: Ключ в props, в котором будет храниться объект данных. ЕслиstateKey
не указан, все ключи объекта данных будут записаны на прямую в props (props.isAjax
,props.response
, ...).propsKey?: string | string[]
: Ключ в props, в котором будет храниться объект со всеми параметрами RemoteDataProvider (второй получаемый аргумент в функцииchildren
при обычном использовании). ЕслиpropsKey
не указан, параметры RDP не будут переданы в props.provider?: RemoteDataProvider
: Компонент, использованный как RemoteDataProvider. По умолчанию, это обычный RemoteDataProvider. Используйте параметрprovider
для установки кастомного RemoteDataProvider из расширений.- Все остальные параметры будут переданы на прямую в RemoteDataProvider, оборачивающий компонент, и будут работать как обычно.
Component: React.ComponentType
: Оборачиваемый компонент.
Обратите внимание, если параметр onlyWithData
в объектах данных задан как true
(или не задан - он по умолчанию true
), то компонент не будет смонтирован до того момента, как данные для всех объектов с onlyWithData == true
не загрузятся.
Как это работает
В этом разделе описывается жизненный цикл данных с использованием remote-data-provider
.
Начало загрузки
Изначально в redux никаких данных нет:
// redux state
{
remoteData: {}
}
Используем наш компонент из базового примера:
<div>
<MyGreatComponent count={1} />
</div>
useRemoteData начинает загружать данные из http://my.perfect.api.com/get/some/data?count=1
и записывает данные в redux:
// redux state
{
remoteData: {
myGreatData: { // параметр reducerKey='myGreatData'
providerId: 's8dfj3nlsi',
isAjax: true,
isEmpty: true,
isError: false,
response: undefined,
request: {
url: 'http://my.perfect.api.com/get/some/data'
params: {
count: 1
}
},
error: undefined
}
}
}
Пока данных нет - isEmpty === false
- наш компонент возвращает null
// result
<div></div>
Успешная загрузка
Когда данные успешно загружены, они записываются в redux:
// redux state
{
remoteData: {
myGreatData: {
isAjax: false,
isEmpty: false,
response: ['somedata'],
// ... остальное без изменений
}
}
}
После чего происходит рендер с данными:
// result
<div>
["somedata"]
</div>
Неудавшаяся загрузка
Когда происходит ошибка загрузки данных, данные об ошибке записываются в redux:
// redux state
{
remoteData: {
myGreatData: {
providerId: 's8dfj3nlsi',
isAjax: false,
isEmpty: true,
isError: true,
response: undefined,
request: {
url: 'http://my.perfect.api.com/get/some/data'
params: {
count: 1
}
},
error: {
status: 404, // статус ошибки, если есть
message: '404 Not Found', // сообщение об ошибке, если есть
data: {} // дополнительные данные (тело ответа), если есть
}
}
}
}
Так как при этом isEmpty === false
, компонент возвращает null
.
Повторный рендер
Теперь, если вызвать этот компонент снова, с идентичными параметрами, повторной загрузки не произойдет:
<div>
<MyGreatComponent count={1} />
</div>
// result
<div>
["somedata"]
</div>
Изменение параметров
Если изменить параметр запроса count
:
<div>
<MyGreatComponent count={3} />
</div>
RemoteDataProvider сравнит объекты запросов из redux
и props
, увидит изменения и начнет перезагружать данные:
// redux state
{
remoteData: {
myGreatData: {
isAjax: true, // началась загрузка
request: {
url: 'http://my.perfect.api.com/get/some/data'
params: {
count: 3 // изменившийся параметр
}
},
// ... остальное без изменений
}
}
}
При этом старые данные все еще будут доступны, и компонент сможет их использовать:
// result
<div>
["somedata"] новые данные уже близко...
</div>
Успешная загрузка новых данных
Когда новые данные будут загружены, они будут записаны в redux:
// redux state
{
remoteData: {
myGreatData: {
isAjax: false,
response: ['somedata', 'somedata', 'somedata'],
// ... остальное без изменений
}
}
}
И компонент будет рендериться с новыми данными:
// result
<div>
["somedata", "somedata", "somedata"]
</div>
Неудавшаяся загрузка новых данных
Если не удалось загрузить новые данные, то данные об ошибке записываются, а старые данные удаляются:
// redux state
{
remoteData: {
myGreatData: {
providerId: 's8dfj3nlsi',
isAjax: false,
isEmpty: true,
isError: true,
response: undefined,
request: {
url: 'http://my.perfect.api.com/get/some/data',
params: {
count: 1
}
},
error: {
status: 404, // статус ошибки, если есть
message: '404 Not Found', // сообщение об ошибке, если есть
data: {} // дополнительные данные об ошибке, если есть
}
}
}
}
Так как при этом isEmpty === false
, компонент возвращает null
.
Композиция провайдеров Composer
Если необходимо для нескольких загружаемых данных иметь одну children
функцию, используйте Composer
. Параметры компонента:
providers: Object | Array
- объект или список, содержащий готовые элементы или параметры (объектов) для провайдера.defaultProvider?: RemoteDataProvider
- провайдер, использующийся для параметров (объектов) вproviders
. По умолчанию обычный RemoteDataProvider. Можно использовать кастомизированные провайдеры из расширений.
import React from 'react'
import { RemoteDataProvider, Composer } from '@aic/react-remote-data-provider'
// ...
<Composer
providers={{
someData: <RemoteDataProvider {...someDataOptions} />, // элемент
otherData: { request, onlyWithData, ... }, // параметры для RDP
}}
>
{({ someData, otherData }) => {
console.log(someData) // => { isEmpty, response, ... }
console.log(otherData) // => { isEmpty, response, ... }
}}
</Composer>
Вместо объекта в данном примере можно использовать массив в качестве параметра providers
, тогда children
функция должна принимать массив, порядок объектов данных которого будет соответствовать порядку в providers
.
Особенности
- Composer начинает загружать все данные одновременно,
children
функция вызывается при каждом изменении, после того, как у всех провайдеров с параметромonlyWithData === true
имеются загруженные данные.
Доступ к данным в redux
Время от времени появляется необходимость получить информацию о загружаемых данных вне remote-data-provider
. Для этого можно использовать стандартный декоратор connect
из redux, получая данные с помощью функции getLocalState
или хук useLocalState
.
Функция getLocalState
import { connect } from 'react-redux'
import { getLocalState } from '@aic/react-remote-data-provider'
function mapStateToProps (state) {
const reducerKey = 'someKey'
const reducerPath = ['some', 'path']
return {
someDataState: getLocalState(state, reducerKey, reducerPath)
}
}
@connect(mapStateToProps)
export class SomeReactComponent extends React.Component {
// ...
}
(подробная информация про декоратор connect
в официальном репозитории)
Функция getLocalState
принимает следующие параметры:
state
- redux state;reducerKey
: Такой же, как вremote-data-provider
, который загружает эти данные;reducerPath
: Такой же, как вremote-data-provider
, который загружает эти данные, может отсутствовать.
Функция возвращает данные, находящиеся по указанным reducerPath
и reducerKey
. Если ничего не найдено, функция возвращает пустой объект данных:
{
providerId: undefined,
response: undefined,
request: undefined,
isEmpty: true,
isAjax: false,
isError: false,
error: undefined
}
Хук useLocalState
Хук работает аналогично getLocalState
в связке с connect
, но без необходимости передавать state
.
import { useLocalState } from '@aic/react-remote-data-provider'
export function SomeReactComponent (props) {
const reducerKey = 'someKey'
const reducerPath = ['some', 'path']
const someDataState = useLocalState(reducerKey, reducerPath)
...
}
Предзагрузка данных
Пакет remote-data-provider
предоставляет два действия:
getData(props): function (dispatch): Promise
- принимаетprops
- все параметрыRemoteDataProvider
, и возвращает функцию, которая загружает данные, записывая в redux информацию о начале загрузке, и в последствии об успешной или неудавшейся загрузке. Возвращает Promise.clearData(props): function (dispatch): void
- принимаетprops
- все параметрыRemoteDataProvider
, и возвращает функцию, которая очищает состояние в state.
Данные действия используются непосредственно в useRemoteData
, по этому, чтобы вызывать их вне компонента, используйте функцию getRDPStaticProps
, которая возвращает все внутренние параметры useRemoteData
. Функция getRDPStaticProps
принимает:
state
: redux state.props
: Внешние параметры дляuseRemoteData
.
Например, мы используем RemoteDataProvider
со следующими параметрами:
const someDataOptions = {
reducerKey: 'someKey',
request: {
url: '/some/url'
}
}
return (
<RemoteDataProvider {...someDataOptions}>
{(remoteData) => { ... }}
</RemoteDataProvider>
)
Предзагрузка данных для этого компонента:
import { getData, getRDPStaticProps } from '@aic/react-remote-data-provider'
import { store } from 'src/store' // ваш redux store
const state = store.getState()
const staticSomeDataProps = getRDPStaticProps(
state,
someDataOptions // RDP параметры из примера выше
)
const loadingPromise = getData(staticSomeDataProps)(store.dispatch) // => Promise
loadingPromise.then(() => {
console.log('Загрузка данных окончена') // данные сохраннены в redux
})
Для получения store
и dispatch
в компоненте, можно использовать хуки useStore
и useDispatch
из redux
:
import React, { useEffect } from 'react'
import { useStore, useDispatch } from 'react-redux'
import { getData, getRDPStaticProps } from '@aic/react-remote-data-provider'
function SomeComponent (props) {
const store = useStore()
const dispatch = useDispatch()
useEffect(() => {
const staticSomeDataProps = getRDPStaticProps(
store.getState(),
someDataOptions // RDP параметры из примера выше
)
const loadingPromise = getData(staticSomeDataProps)(dispatch) // => Promise
loadingPromise.then(() => console.log('Загрузка данных окончена'))
}, []) // загружаем данные один раз
// ...
}
Предзагрузка данных ведет себя так же, как если бы данные загружал RemoteDataProvider
. Данные записываются в redux, отрабатывают все middleware, и т.д.
Когда вы используете RemoteDataProvider
с параметрами someDataOptions
(из примера), у него уже будут все данные, и он не будет повторно их загружать.
Подробнее про действие getData
в API reference
Расширения для remote-data-provider
Расширения позволяют увеличить гибкость и базовый функционал remote-data-provider. Обычно, расширения подключаются с помощью redux middleware, и могут предоставлять компоненты-обертки над стандартным RemoteDataProvider. Пакет remote-data-provider предоставляет несколько расширений. Те расширения, которые вы не используете, не попадают в финальную сборку вашего приложения.
Вы также можете сами создавать расширения для remote-data-provider.
Расширение collector
Данное расширение позволяет реализовывать подзагрузку данных, совмещая старые и новые данные.
Предназначение
Нередко возникает потребность разделить данные, получаемые из API, на части, и в последствии догружать их. Расширение collector
упрощает дозагрузку и совмещение данных для таких случаев.
Подключение
Подключите collectorMiddleware
к вашему store как middleware
// configureStore.js
import { createStore, applyMiddleware, compose } from 'redux'
import { collectorMiddleware } from '@aic/react-remote-data-provider/extensions/collector'
export default function configureStore () {
// ...
const store = compose(
applyMiddleware(collectorMiddleware())
// ...
)(createStore)(rootReducer, initialState)
return store
}
Подробнее про collectorMiddleware
в API reference
Компонент RemoteDataProviderCollector
После подключения collectorMiddleware
, вы можете использовать RemoteDataProviderCollector
для подзагрузки и совмещения данных. Этот компонент является оберткой над обычным RemoteDataProvider, имея такие же параметры, плюс несколько новых:
changeableRequest: object
- часть обычногоrequest
, которую можно впоследствии изменять, при этом данные будут собираться вместе; обязательный параметр.path?: Array<string> | string
- путь вresponse
, в котором будут собираться данные. Если не указан, данные будут собираться в корнеresponse
.unshift?: boolean
- еслиtrue
, новые данные будут добавляться перед старыми (в начало), иначе - наоборот (по умолчаниюfalse
).
При этом, RemoteDataProviderCollector передает во второй аргумент children
функции метод setChangeableRequest
, позволяющий устанавливать новый changeableRequest
.
Пример использования:
import { RemoteDataProviderCollector } from '@aic/react-remote-data-provider/extensions/collector'
const props = {
reducerKey: 'someKey',
request: {
url: '/api/users',
params: {
permission: 'admin'
}
},
changeableRequest: { // начальный changeableRequest
params: {
page: 1
}
},
path: 'user',
// остальные RemoteDataProvider props
}
<RemoteDataProviderCollector {...props}>
{({ response, request }, { setChangeableRequest }) => {
console.log('response:', response)
console.log('request:', request)
const loadNextPage = () => setChangeableRequest({
params: {
page: request.params.page + 1
}
})
return <button onClick={loadNextPage}>load next page</button>
}}
</RemoteDataProviderCollector>
// выведет в консоль:
// 'response:'
{
user: [ // данные собираются тут (path === 'user')
{ /* некоторая информация о пользователе со страницы 1 */ }
],
// остальные данные ответа только со страницы 1
}
// 'request:'
{
url: '/api/users',
params: {
permission: 'admin',
page: 1 // добавлено из `changeableRequest`
}
}
// выведет в консоль после нажатия на кнопку 'load next page' и дозагрузки страницы 2:
// 'response:'
{
user: [
{ /* некоторая информация о пользователе со страницы 1 */ },
{ /* некоторая информация о пользователе со страницы 2 */ }
],
// остальные данные ответа только со страницы 2
}
// 'request:'
{
url: '/api/users',
params: {
permission: 'admin',
page: 2 // добавлено из `changeableRequest`
}
}
Особенности
- Параметр
changeableRequest
задает только начальные изменяющиеся параметры запроса. После использования методаsetChangeableRequest
вrequest
попадают только новые установленные параметры. - Используемый
changeableRequest
является частью конечногоrequest
, но также его можно получить во втором параметреchildren
функции:({ request, ... }, { setChangeableRequest, changeableRequest }) => { ... }
- Аргумент метода
setChangeableRequest
полностью заменяетchangeableRequest
, дублируйте параметры, даже если они не изменились. Например:setChangeableRequest({ ...changeableRequest, someParam: 'new value' })
- Для корректной работы, ключи объекта
changeableRequest
не должны меняться. Если какой-то параметр изначально не используется, но в будущем изменится, задайте егоundefined
:changeableRequest={{ params: { someParam: 'value', otherParam: undefined } }}
- После того, как компонент отмонтируется (например, вы перейдете на другую страницу), а потом обратно примонтируется (вернетесь обратно на предыдущую страницу) -
RemoteDataProviderCollector
восстановит последнийchangeableRequest
из redux state. Для полного восстановления, необходимо, чтобы ключиchangeableRequest
не менялись (см. предыдущий пункт). - Если данные по ключу
path
вresponse
- не массив, то они оборачиваются в массив. Например, еслиpath === 'user'
, аresponse === { user: { name: 'Dan', ... } }
-response
преобразуется в{ user: [{ name: 'Dan', ... }] }
. Если в текущем примереpath
не задан,response
преобразуется в[{ user: { name: 'Dan', ... } }]
Подробнее про RemoteDataProviderCollector
в API reference
Расширенное использование
collectorMiddleware
добавляет несколько дополнительных параметров для обычного RemoteDataProvider
:
exCollectorChangeableRequest?: Array< string | Array<string> >
- массив ключей вrequest
, которые в нем могут меняться от запроса к запросу. При новой загрузке данных, если есть предыдущий результат, и вrequest
поменялись значения только у ключей данного параметра, старые данные будут добавлены к новым. Иначе в redux будут записаны только новые данные. Если данный параметр не указан, все будет работать как обычно.
Ключи могут быть строками ('param'
,'some.deep.param'
,'some.array[0]'
) или массивами (['param']
,['some', 'deep', 'param']
,['some', 'array', '0']
).exCollectorPath?: Array<string> | string
- путь вresponse
, в котором будут собираться данные. Если не указан, данные будут собираться в корнеresponse
.
Если данные по собираемому пути - не массив, то они оборачиваются в массив. Например, еслиexCollectorPath === 'user'
, аresponse === { user: { name: 'Dan', ... } }
-response
преобразуется в{ user: [{ name: 'Dan', ... }] }
. Если в текущем примереexCollectorPath
не задан,response
преобразуется в[{ user: { name: 'Dan', ... } }]
.
Данный параметр работет, только если указанexCollectorChangeableRequest
, иначе игнорируется и преобразования не происходит.exCollectorUnshift?: boolean
- еслиtrue
, новые данные будут добавляться перед старыми (в начало), иначе - наоборот (по умолчаниюfalse
).
Компонент-обертка RemoteDataProviderCollector
использует эти параметры для своей работы. При необходимости, вы можете использовать их напрямую в обычном RemoteDataProvider
, или разработать свой компонент-обертку.
Расширение globalError
Данное расширение позволяет собирать ошибки при загрузке данных любого RemoteDataProvider
компонента в одном месте и получать их.
Предназначение
Хранение ошибок в одном месте упрощает ведение статистики, выдачу окна ошибки или страницы, описывающей ошибку.
Подключение
Подключите globalErrorMiddleware
к вашему store как middleware. Данная функция принимает один не обязательный параметр - объект, содержащий настройки для middleware:
reduxKey?: string = '_globalError'
- этот параметр определяет место храния ошибок в redux. Ошибки хранятся внутриremoteData
, по этому, вы не можете использовать значение этого параметра как параметрreducerKey
в компонентеRemoteDataProvider
. По умолчанию он равен'_globalError'
. Меняйте этот параметр, только если есть пересечения в названиях.setErrorKey?: string = 'exGlobalError'
- ключ для параметра компонентаRemoteDataProvider
. Меняйте этот параметр, если у вас подключено несколькоglobalErrorMiddleware
, или этот ключ пересекается с другим параметром. Далее в описании будет использоваться стандартное значение -'exGlobalError'
setByDefault?: boolean = false
- этот параметр определяетexGlobalError
(может называться иначе, если измененsetErrorKey
) по умолчанию, если он не определен в параметрахRemoteDataProvider
компонента.
// configureStore.js
import { createStore, applyMiddleware, compose } from 'redux'
import { collectorMiddleware } from '@aic/react-remote-data-provider/extensions/globalError'
export default function configureStore () {
// ...
const store = compose(
applyMiddleware(
globalErrorMiddleware(), // без параметров
// или
globalErrorMiddleware({ // с параметрами
reduxKey: '_globalError',
setByDefault: false
})
)
// ...
)(createStore)(rootReducer, initialState)
return store
}
Подробнее про globalErrorMiddleware
в API reference
Использование
globalErrorMiddleware
добавляет для компонента RemoteDataProvider
параметр exGlobalError
(может называться иначе, если установлено значение setErrorKey
в настройках middleware). Если данный параметр равен true
, то ошибки загрузки, вызванные этим компонентом, будут собираться в глобальном хранилище redux
.
<RemoteDataProvider
// ... обычные параметры
exGlobalError
/> // ошибки будут собираться в глобальном хранилище
<RemoteDataProvider
// ... обычные параметры
/> // ошибки НЕ будут собираться в глобальном хранилище
Если вы установили значение setByDefault === true
в настройках globalErrorMiddleware
, то по умолчанию ошибки будут записываться в глобальное хранилище с любого RemoteDataProvider
. Чтобы не собирать ошибки для отдельного компонента, установите значение exGlobalError
в false
:
<RemoteDataProvider
// ... обычные параметры
/> // ошибки будут собираться в глобальном хранилище
<RemoteDataProvider
// ... обычные параметры
exGlobalError={false}
/> // ошибки НЕ будут собираться в глобальном хранилище
Получение и очистка ошибок
Для получения ошибок из глобального хранилища, используйте функции getGlobalErrors
и hasGlobalError
. Обе функции имеют одинаковые аргументы:
state
- redux statereduxKey = '_globalError'
- должен быть аналогичным значениюreduxKey
из настроекglobalErrorMiddleware
. Если вы не устанавливали данную настройку для middleware, не используйте этот параметр.
Функция getGlobalErrors
возвращает массив собранных ошибок, каждая из которых имеет такой формат:
{
reducerKey: string,
reducerPath?: string,
error: {
status?: number,
message?: string,
data?: any
}
}
reducerKey
и reducerPath
равны соответствующим параметрам компонента RemoteDataProvider
, в котором произошла ошибка.
Функция hasGlobalError
возвращает true
, если есть хотя бы одна ошибка в глобальном хранилище, иначе false
.
Для очистки глобального хранилища от всех ошибок, используйте функцию clearGlobalError
, которая создает действие. Используйте его в dispatch
, и глобальное хранилище очистится.
import { connect } from 'react-redux'
import { hasGlobalError, getGlobalErrors, clearGlobalError } from '@aic/react-remote-data-provider/extensions/globalError'
function mapStateToProps (state) {
return {
isGlobalError: hasGlobalError(state),
globalErrors: getGlobalErrors(state)
}
}
function mapDispatchToProps (dispatch) {
clearGlobalError: () => dispatch(clearGlobalError())
}
@connect(mapStateToProps, mapDispatchToProps)
export class AppErrorWrapper extends React.Component {
render () {
const { isGlobalError, globalErrors, clearGlobalError } = this.props
console.log(isGlobalError) // => true
console.log(globalErrors) // => [{ reducerKey: 'someKey', error: { status: 404 } }]
if (isGlobalError) {
return (
<div>
<p>Упс, что-то пошло не так...</p>
<p>Ошибок на странице: {globalErrors.length}</p>
<button onClick={clearGlobalError}>
попробовать еще раз
</button>
</div>
)
} else {
return <App />
}
}
}
Подробнее про расширение globalError
в API reference
Расширение serverReRender
Данное расширение позволяет организовывать асинхронные действия во время SSR (Server Side Render), включая загрузку данных с помощью RemoteDataProvider
.
Предназначение
react-dom/server
при рендере сам по себе не позволяет производить асинхронные действия, которые часто требуются для загрузки данных и правильного отображения страницы. Это расширение позволяет проводить асинхронные операции между синхронными рендерами react-dom/server
.
Подключение
Подключите asyncActionMiddleware
к вашему store
как middleware. Данная функция принимает один обязательный параметр - экземпляр класса AsyncActionController
(будет рассмотрен позже).
// configureStore.js
import { createStore, applyMiddleware, compose } from 'redux'
import { asyncActionMiddleware } from '@aic/react-remote-data-provider/extensions/serverReRender'
export default function configureStore (asyncActionController) { // принимаем контроллер извне
// ...
const store = compose(
applyMiddleware(asyncActionMiddleware(asyncActionController))
// ...
)(createStore)(rootReducer, initialState)
return store
}
Использование serverReRender
asyncActionMiddleware
позволяет отслеживать загрузки данных внутри компонентов RemoteDataProvider
с помощью AsyncActionController
. Теперь, для перерисовок между загрузками данных, необходимо использовать функцию serverReRender(options: object): Promise<object>
. Возможные параметры аргумента options
:
maxRenders?: number = 10
- максимальное количество перерисовок, по умолчанию 10.renderTimeout?: number = 5000
- максимальное время (в миллисекундах) для загрузки данных во время каждой отдельной перерисовки, по умолчанию 5 секунд.controller: AsyncActionController
- экземаляр классаAsyncActionController
, должен быть тем же самым экземпляром, который использован вasyncActionMiddleware
.render: (next, stop)
- функция рендера, вызывающаяся при каждой перерисовке. Принимает аргументами две функции, одну из которых вы обязаны вызвать:next(result: any): void
- заканчивает данную перерисовку с результатом рендера.stop(result?: any): void
- заканчивает все перерисовки. Если указанresult
, он заменяет собой последний результат, если не указан - используется предыдущий результат (при последнем вызове функцииnext
).
Функция возвращает промис (Promise), который всегда выполняется успешно, и содержащий объект со следующими полями:
result: any
- аргумент, переданный в функциюnext
при последнем ее вызове. Если была использована функцияstop
с указанным аргументомresult
, будет использован он.error?: SSRRenderCountError | SSRTimeoutError | any
- ошибка, произошедшая во время перерисовок. Если ошибок не произошло, будет равенundefined
. Ошибка может произойти в следующих случаях:- Произошла ошибка во время выполнения функции
render
. - Превышено максимальное количество перерисовок (параметр
maxRenders
) - тогда ошибка будет создана с помощьюSSRRenderCountError
. - Превышено максимальное время ожидания загрузки (параметр
renderTimeout
) - тогда ошибка будет создана с помощьюSSRTimeoutError
.
- Произошла ошибка во время выполнения функции
stats: { renders: number, actions: number }
- статистика, содержащая общее количество перерисовок (renders
) и выполненных асинхронных действий (actions
).
Пример использования с express
и react-router-dom
:
// server.js
import React from 'react'
import { renderToString } from 'react-dom/server'
import { StaticRouter as Router } from 'react-router-dom'
import configureStore from 'services/store/configureStore' // ваш configuireStore
import { Provider } from 'react-redux'
import { AsyncActionController, serverReRender, SSRRenderCountError, SSRTimeoutError } from '@aic/react-remote-data-provider/extensions/serverReRender'
import App from '../src/App' // компонент для рендера приложения
import App from '../src/Html' // компонент для рендера HTML
const app = express()
app.use((req, res) => {
// создаем экземпляр AsyncActionController
const asyncActionController = new AsyncActionController()
// передаем экземпляр AsyncActionController для asyncActionMiddleware при создании redux store
const store = configureStore(asyncActionController)
// контекст для static router (см. пакет 'react-router-dom')
const context = {}
serverReRender({
maxRenders: 10,
renderTimeout: 5000
controller: asyncActionController,
render: (next, stop) => { // функция, вызывающаяся при каждой перерисовке
// прерываем перерисовки, если происходит редирект
if (context.url) {
return stop()
}
return next( // передаем результат на каждой итерации
renderToString(
<Provider store={store}> // store единый для всех перерисовок
<Router context={context} location={req.url}>
<App />
</Router>
</Provider>
)
)
}
}).then(({ result, error, stats }) => {
if (context.url) { // производим редирект, если он есть
res.redirect(301, context.url)
return
}
if (error) { // если имеется ошибка во время рендера
if (error instanceof SSRRenderCountError) {
console.error('Количество перерисовок превысило максимум')
} else if (error instanceof SSRTimeoutError) {
console.error('Превышено максимальное время ожидания между перерисовками')
}
const HTML = <Html result={renderToString(<h1>Ошибка!</h1>)} />
res.status(500).send(HTML)
return
}
// успешный рендер HTML
const HTML = renderToString(<Html result={result} state={store.getState()} />)
res.status(200).send(HTML)
})
})
После всех перерисовок все загруженные данные будут храниться в redux store. Для корректной перерисовки в браузере, необходимо передать вместе с HTML состояние redux store. Подробнее об этом описано тут.
Как это работает
Каждая итерация перерисовки происходит следующим образом:
- Происходит рендер главного компонента.
- Если во время рендера используются компоненты
RemoteDataProvider
, они начинают загрузку данных в redux store (если данных еще нет). - Когда начинается (и заканчивается) загрузка данных в
RemoteDataProvider
,asyncActionMiddleware
начинает (и завершает при окончании) асинхронное действие в контроллере - экземпляре классаAsyncActionController
.
- Если во время рендера используются компоненты
- После рендера,
serverReRender
, проверяет, имеются ли активные асинхронные действия (загрузки) в контроллере. Если они есть, происходит ожидание окончания всех загрузок. - Если во время рендера происходили асинхронные действия, после ожидания начинается новая перерисовка. Иначе перерисовки заканчиваются, и промис выполняется с результатом.
Перерисовки заканчиваются преждевременно (вне зависимости от асинхронных действий), если происходит ошибка (передаваемый параметр error
).
Так как все данные о загрузках сохраняются в redux store, у компонентов RemoteDataProvider
появляются данные, и повторной перезагрузки на каждый рендер не происходит.
Вложенные друг в друга RemoteDataProvider
компоненты учитываются (для них происходит загрузка данных), однако увеличивается количество перерисовок, если вы не монтируете вложенные RemoteDataProvider
при отсутствии данных у верхнего компонента.
Расширенное использование
Вы можете использовать экземпляр класса AsyncActionController
самостоятельно, для добавления асинхронных действий во время перерисовок. Действия, добавленные самостоятельно, точно также учитываются во время перерисовок, как и RemoteDataProvider
.
Класс AsyncActionController
В основе расширения используется класс AsyncActionController
, помогающий отслеживать начало и завершение асинхронных событий. Каждое асинхронное действие должно иметь уникальный id
. API класса:
new AsyncActionController(options?: object)
- конструктор класса.options
- не обязательный аргумент, и может содержать следующие параметры:maxAsyncActionDuration?: number = 0
- максимальная длительность (в миллисекундах) каждого асинхронного действия, после чего оно будет автоматически завершено с ошибкой. Если установлено в 0 (по умолчанию), длительность не ограничена.serverOnly?: boolean = true
- еслиtrue
(по умолчанию), будет отслеживать асинхронные действия только на сервере, на клиенте все асинхронные действия будут игнорироваться.
isDisabled(): boolean
- возвращаетtrue
, если контроллер не отслеживает асинхронные действия (параметр конструктораserverOnly === true
и код выполняется в браузере).startAction(id: string | number): void
- начинает асинхронное действие с даннымid
. Если действие уже было начато (или завершено), ничего не происходит.completeAction(id: string | number, error?: any): void
- заканчивает асинхронное действие с даннымid
. Если указанerror
, он записывается, как ошибка данного действия.completeAllActions(): void
- заканчивает все асинхронные действия. Используйте эту функцию, только если точно знаете, что все загрузки окончены. Обычно в ней нет необходимости.createActionInstance(id: string | number): { start, complete }
- создает независимый от класса экземпляр асинхронного действия, привязанного к данномуid
. Фактически, это синтаксический сахар для передачи функцийstartAction
иcompleteAction
независимо от класса и привязанными кid
:start(): void
- привязка функции() => this.startAction(id)
complete(error?: any): void
- привязка функции(error) => this.completeAction(id, error)
getCompletePromise(): Promise<void>
- возвращает промис, который выполняется, когда все асинхронные действия будут окончены.
Если до выполнения данного промиса будут добавлены новые асинхронные действия, они будут учтены в данном промисе (рекурсивно).
Этот промис всегда выполняется успешно (resolve). ЕслиisDisabled() === true
, промис всегда будет сразу выполнен.getActionPromise(id: string | number): Promise<void>
- возвращает промис, который выполняется, когда заканчивается действие с даннымid
. Если действия с такимid
не существует, или оно завершено, промис будет сразу выполнен (Promise.resolve()
).
Этот промис всегда выполняется успешно (resolve). ЕслиisDisabled() === true
, промис всегда будет сразу выполнен.hasAction(id: string | number): boolean
- возвращаетtrue
, если существует асинхронное действие с такимid
.
ЕслиisDisabled() === true
, всегда возвращаетfalse
.hasIncompleteActions(): boolean
- возвращаетtrue
, если остались не завершенные действия.
ЕслиisDisabled() === true
, всегда возвращаетfalse
.
Пример использования в компонентах
Если необходимо совершить какое-либо асинхронное действие в компоненте, используйте AsyncActionProvider
и withAsyncAction
для передачи и получения контроллера соответственно.
// index.js
import { AsyncActionProvider } from '@aic/react-remote-data-provider/extensions/serverReRender'
...
// asyncActionController - экземплят AsyncActionController
<AsyncActionProvider controller={asyncActionController}>
<App />
</AsyncActionProvider>
// SomeComponent.js
import React, { Component } from 'react'
import { withAsyncAction } from '@aic/react-remote-data-provider/extensions/serverReRender'
import { connect } from 'react-redux'
@connect(
state => ({ someData: state.someData }),
dispatch => ({ saveSomeData: payload => dispatch({ type: 'SAVE_SOME_DATA', payload }) })
)
@withAsyncAction('controller') // ключ в props с контроллером, по умолчанию 'asyncActionController'
class SomeComponent extends Component {
id = 'SomeComponentId' // уникальный id загрузки для контроллера
componentWillMount () {
if (!this.props.someData) {
this.props.controller.startAction(this.id)
this.loadSomeData().then(data => {
this.saveSomeData(data)
this.props.controller.completeAction(this.id)
})
}
}
loadSomeData () {
return new Promise(resolve => {
// ... load some data here
})
}
}
Пример использования в действиях (с redux-thunk
)
Пакет redux-thunk позволяет "пробрасывать" в redux действия дополнительный аргумент. Воспользуемся этим, чтобы использовать экземпляр AsyncActionController
в redux действиях.
// configuireStore.js
import { createStore, applyMiddleware, compose } from 'redux'
import { asyncActionMiddleware } from '@aic/react-remote-data-provider/extensions/serverReRender'
import thunkMiddleware from 'redux-thunk'
export default function configureStore (asyncActionController) { // принимаем контроллер извне
// ...
const store = compose(
applyMiddleware(asyncActionMiddleware(asyncActionController)),
applyMiddleware(
// добавляем экстра аргумент для redux-thunk
thunkMiddleware.withExtraArgument(asyncActionController)
)
// ...
)(createStore)(rootReducer, initialState)
return store
}
Теперь, можно использовать контроллер в redux действиях.
// loadSomeDataAction.js
export function loadSomeDataActionCreator () {
return (dispatch, getState, actionController) => {
// уникальный id действия для контроллера, можно генерировать
const id = 'loadSomeDataAction_id'
actionController.startAction(id) // начинаем асинхронное действие
dispatch({
type: 'LOAD_SOME_DATA_START'
})
axios.get('/some/data').then(response => {
dispatch({
type: 'LOAD_SOME_DATA_END',
payload: response.data
})
asyncAction.completeAction(id) // заканчиваем асинхронное действие
})
}
}