prostore-react
Данный адаптер предоставляет набор полезных инструментов
для интеграции prostore
в React
.
Можно прокидывать сторы через контекст и
подписываться на сторы с помощью хуков,
а также автоматически выполнять запросы в RequestStore
.
Использование
Для примеров ниже будет использован простой стор,
считающий количество вызовов функции increment
:
import { BehaviorStore } from '@proscom/prostore';
export interface IncStoreState {
number: number;
}
const initialState: IncStoreState = {
number: 0
};
export class IncStore extends BehaviorStore<IncStoreState> {
constructor() {
super(initialState);
}
increment() {
this.setState({
number: this.state.number + 1
});
}
}
import { IncStore } from './IncStore';
export const incStore = new IncStore();
См. пример в CodeSandbox
useStore
Основной хук, который позволяет подписать компонент на
все изменения стора
import { useStore } from '@proscom/prostore-react';
import { incStore } from './incStore';
function MyComponent() {
const [state, store] = useStore(incStore);
const onClick = () => {
store.increment();
};
return <button onClick={onClick}>{state.number}</button>;
}
См. пример в CodeSandbox
useStoreState
Иногда сам стор не нужен, а нужно только его состояние.
Тогда можно использовать этот хук, чтобы не делать лишнюю
деструктуризацию:
function MyComponent() {
const state = useStoreState(incStore);
return <div>{state.number}</div>;
}
См. пример в CodeSandbox
ProstoreContext
Контекст позволяет прокидывать инстансы сторов в компоненты
по ключам. Таким образом реализуется принцип Dependency Injection
и снижается связанность компонентов и сторов.
export const STORE_TEST = 'STORE_TEST';
import ReactDOM from 'react-dom';
import { STORE_TEST } from './stores';
import { IncStore } from './IncStore';
import { App } from './App';
import { ProstoreContext } from '@proscom/prostore-react';
const testStore = new IncStore();
const stores = {
[STORE_TEST]: testStore
};
ReactDOM.render(
<ProstoreContext.Provider value={stores}>
<App />
</ProstoreContext.Provider>,
document.getElementById('root')
);
import { useStore } from '@proscom/prostore-react';
import { IncStore } from './IncStore';
import { STORE_TEST } from './stores';
function App() {
const [state, store] = useStore<IncStore>(STORE_TEST);
const onClick = () => {
store.increment();
};
return <button onClick={onClick}>{state.number}</button>;
}
См. пример в CodeSandbox
useContextStore
Иногда состояние стора в компоненте вообще не нужно,
а нужен только сам стор для вызова его методов.
При использовании сторов как глобальных переменных можно просто
вызывать их методы:
import { incStore } from './incStore';
function IncButton() {
return <button onClick={() => incStore.increment()}>Increment</button>;
}
См. пример в CodeSandbox
При использовании контекста можно получить доступ к стору
с помощью хука useContextStore
. По сравнению с использованием
useStore
это избавит компонент от лишних перерендеров при изменении
состояния стора.
import { useContextStore } from '@proscom/prostore-react';
import { IncStore } from './IncStore';
import { STORE_TEST } from './stores';
function IncButton() {
const incStore = useContextStore<IncStore>(STORE_TEST);
return <button onClick={() => incStore.increment()}>Increment</button>;
}
См. пример в CodeSandbox
useRequestStore
При использовании RequestStore можно воспользоваться специальным
хуком useRequestStore
, чтобы автоматически выполнять запрос
при изменении его переменных, а также отслеживать состояние запроса
(идет загрузка, ошибка, готов результат).
import { AxiosQueryStore } from '@proscom/prostore-axios';
import { useRequestStore } from '@proscom/prostore-react';
export interface DataQueryStoreVariables {
params?: {
page?: number;
};
}
export interface DataQueryStoreData {
message: string;
}
const store = new AxiosQueryStore<DataQueryStoreVariables, DataQueryStoreData>({
client: axios.create(),
query: {
url: '/data.json',
method: 'get'
}
});
function MyComponent() {
const variables = {
params: {
page: 1
}
};
const options = undefined;
const query = useRequestStore(store, variables, options);
const { check, state, load } = query;
if (check.spinner) {
return <Spinner />;
} else if (check.error) {
return <ErrorMessage error={state.error} />;
}
return <Info data={state.data} refresh={load} />;
}
См. пример в CodeSandbox
При каждом рендере useRequestStore
глубоко сравнивает
предыдущие переменные с новыми, и если есть отличия, то
вызывает выполнение запроса с новыми переменными.
Под глубоким сравнением понимается
lodash.isEqual
.
options
передаются вторым аргументом в store.loadData
в неизменном виде. При изменении options
запрос не будет повторен.
useRequestStore
можно также использовать с контекстом.
В таком случае необходимо указать тип переменных и тип результата аргументами дженерика:
import { useRequestStore } from '@proscom/prostore-react';
import { STORE_TEST } from './stores';
import { StoreTestVariables, StoreTestData } from './stores/StoreTest';
function MyComponent() {
const variables = {};
const query = useRequestStore<StoreTestVariables, StoreTestData>(
STORE_TEST,
variables
);
}
См. пример в CodeSandbox
useAsyncOperation
RequestStore
представляет собой реактивную зависимость результата запроса от переменных (параметров запроса).
Это значит, что момент выполнения запроса определяется автоматически.
Такая семантика подходит только запросов, удовлетворяющим свойствам чистой функции
(возвращающих одни и те же значения для одних и тех же переменных и не выполняющим побочных эффектов),
например для GET HTTP запросов на получение данных и запросов GraphQL Query.
Мутирующие запросы (POST HTTP или GraphQL Mutation) следует вызывать в коде императивно
в нужный момент (например, в ответ на клик пользователя по кнопке).
Мутирующий запрос может не оказывать никакого влияния на состояние компонентов, тогда
его можно вызывать просто напрямую без использования хуков из этой библиотеки.
Если же возникает потребность отслеживать статус выполнения мутирующего запроса, то
для этого можно использовать хук useAsyncOperation
:
import axios from 'axios';
import { useAsyncOperation, AsyncSingletonError } from '@proscom/prostore-react';
function MyComponent() {
const saveOp = useAsyncOperation(
() => {
return axios.post('/save', data).catch((e) => console.error(e));
},
{
finishedTimeout: 5000,
singleton: true
}
);
const {
run,
loading,
finished,
setFinished
} = saveOp;
const handleClick = () => {
run().catch((err) => {
if (err instanceof AsyncSingletonError) return;
console.error(err);
});
};
return (
<button type="button" onClick={handleClick} disabled={loading}>
{finished ? 'Saved' : loading ? 'Saving...' : 'Save'}
</button>
);
}
См. пример в CodeSandbox
usePropsObservable
Преобразовывает пропы компонента или любые другие данные, связанные с циклом рендера реакта,
в Observable из rxjs.
Этот Observable затем можно использовать для построения реактивных пайплайнов, и дальнейшей
подписки этого или другого компонента на результат.
Обратите внимание, что если подписать тот же самый компонент, на обзервабл, который возвращается
из этого хука, то это может привести к бесконечному циклу перерендеров, если передаваемые
пропы не стабилизируются. При передаче объект с пропами поверхностно проверяется
(см. shallowequal).
Если ни один ключ не изменился, то обзервабл не будет эмитить новое значение.
Так как этот хук достаточно сложен для понимания и несет за собой накладные расходы при выполнении,
то использовать его следует только в том случае, если нужно связать компонент с каким-то существующим
обзерваблом, либо использовать специфичный функционал rxjs.
import {
useObservableState,
usePropsObservable
} from '@proscom/prostore-react';
import React, { useState } from 'react';
import { debounceTime, map, tap } from 'rxjs/operators';
export default function App() {
const [search, setSearch] = useState('');
console.log('render', search);
const debounced$ = usePropsObservable(
{ search },
(props$) => {
return props$.pipe(
debounceTime(500),
map((p) => p.search),
tap((v) => console.log('debounced', v))
);
},
[]
);
const debouncedSearch = useObservableState(debounced$);
return (
<div>
<div>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
</div>
<div>{debouncedSearch}</div>
</div>
);
}
См. пример в CodeSandbox
useObservable
Этот хук позволяет подписать колбек на произвольный обзервабл.
import { useObservable } from '@proscom/prostore-react';
import { fromEvent } from 'rxjs';
const resize$ = fromEvent(window, 'resize');
function MyComponent() {
const handleChange = useCallback((value) => {
console.log('changed', value);
});
useObservable(resize$, handleChange);
}
useObservableCallback
Этот хук позволяет подписаться на произвольный обзервабл, но не выполняет переподписку
при изменении колбека. Поэтому пример выше может быть переписан следующим образом:
import { useObservableCallback } from '@proscom/prostore-react';
import { fromEvent } from 'rxjs';
const resize$ = fromEvent(window, 'resize');
function MyComponent() {
useObservableCallback(resize$, (value) => {
console.log('changed', value);
});
}
Этот хук полезен при создании подписки на события сторов, а не на их состояния.
См. пример в документации prostore
.
useObservableState
Этот хук позволяет подписать компонент на произвольный обзервабл.
При наступлении события (изменении обзервабла), его данные будут сохранены в стейт компонента,
а компонент перерендерится.
Убедитесь, что используемый обзервабл повторяет своё последнее значение при подписке,
в противном случае в редких ситуациях часть значений может быть потеряна.
Чтобы создать обзервабл, повторяющий свои значения, воспользуйтесь оператором
shareReplay
. BehaviorSubject
повторяет свои значения при подписке.
import { useObservableState } from '@proscom/prostore-react';
import { fromEvent } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
const windowSize$ = fromEvent(window, 'resize').pipe(
map(() => window.innerWidth),
shareReplay()
);
function MyComponent() {
const windowWidth = useObservableState(windowSize$, window.innerWidth);
}
useSubject
Этот хук аналогичен useObservableState
, только принимает не просто Observable
,
а кастомный тип ObservableWithValue
, и автоматически задает первоначальное значение стейта компонента.
Убедитесь, что используемый обзервабл повторяет своё последнее значение при подписке,
в противном случае в редких ситуациях часть значений может быть потеряна.
Чтобы создать обзервабл, повторяющий свои значения, воспользуйтесь оператором
shareReplay
. BehaviorSubject
повторяет свои значения при подписке.
import { useSubject } from '@proscom/prostore-react';
import { BehaviorSubject } from 'rxjs';
const data$ = new BehaviorSubject(5);
function MyComponent() {
const data = useSubject(data$);
}