Пример 201: Зависимый от нескольких синалов ресурс
Структура директорий выбрана условно для понимания идеи.
Сервис secondary - его внутренний ресурс подписан на выбор персоны из списка который пришел в ответе на запрос сервиса firstly (его внутренний ресурс триггерится собственным счетчиком, который меняет сам пользователь по кнопке Refresh).
tsx
import baseClasses from '~/ui.common.module.scss'
import btnClasses from '~/ui.button.module.scss'
import clsx from 'clsx'
import { useUserInfoService, observer, useSecondaryService } from './store'
const BASE_API_URL = import.meta.env.VITE_BASE_API_URL
export const Example201 = observer(() => {
const userInfo = useUserInfoService()
const secondaryService = useSecondaryService()
return (
<div
className={clsx(
baseClasses.unit,
baseClasses['unit--wide'],
baseClasses.stack2)
}
>
<div
className={baseClasses.absoluteUnitLabel}
title='Chain of Resources & observer'
>
Chain of Resources & observer
</div>
<code>{BASE_API_URL}</code>
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
<button onClick={() => userInfo.inc()} className={clsx(btnClasses.btn, btnClasses.neonBtn, btnClasses['neonBtn--primary'], btnClasses['neonBtn--outlined'])}>Refresh account data</button>
{
userInfo.personList.value.map((p) => (
<button
key={p.id}
onClick={() => userInfo.setActivePersonId(p.id)}
className={
clsx(
btnClasses.btn,
btnClasses.neonBtn,
btnClasses['neonBtn--secondary'],
{
[btnClasses['neonBtn--contained']]: !!userInfo.activePersonId.value && p.id === userInfo.activePersonId.value,
[btnClasses['neonBtn--outlined']]: p.id !== userInfo.activePersonId.value,
},
)
}
>{p.name}</button>
))
}
</div>
<div className={baseClasses.unitInternalWrapper}>
<div className={baseClasses.stack2}>
<div>{userInfo.apiState.loading ? '🟡 loading...' : !!userInfo.apiState.data ? '🟢 ok' : !!userInfo.apiState.error ? `🔴 err | ${userInfo.apiState.error?.message || 'No error msg'}` : '⚪'}</div>
<pre className={baseClasses.preNormalizedMin}>{JSON.stringify({ data: userInfo.apiState.data }, null, 2)}</pre>
</div>
<div className={baseClasses.stack2}>
<div>{secondaryService.apiState.loading ? '🟡 loading...' : !!secondaryService.apiState.data ? '🟢 ok' : !!secondaryService.apiState.error ? `🔴 err | ${secondaryService.apiState.error?.message || 'No error msg'}` : '⚪'}</div>
<pre className={baseClasses.preNormalizedMin}>{JSON.stringify({ data: secondaryService.apiState.data }, null, 2)}</pre>
</div>
</div>
</div>
)
})service.firstly.ts - подписан на счетчик.
ts
import { AbstractService } from '@pravosleva/reactive-engine'
const BASE_API_URL = import.meta.env.VITE_BASE_API_URL
type TPerson = {
id: string;
name: string;
}
export class UserInfoService extends AbstractService {
// -- NOTE: DERTY_MISTAKE 2/3 ✅ Correct way (variant 2)
// constructor(engine: ReactiveEngine) {
// super(engine);
// this.engine.effect(() => {
// // Подписываемся на изменение counter
// const _ = this.counter.value;
// // Синхронно сбрасываем выбранного пользователя при каждом изменении счетчика
// this.engine.untrack(() => this.resetActivePersonId());
// });
// }
// --
public counter = this.engine.signal<number>(0, 'example-201:UserInfoService:signal:counter');
public doubledCounter = this.engine.computed<number>(() => this.counter.value * 2, 'example-201:UserInfoService:computed:counter');
public activePersonId = this.engine.signal<string | null>(null)
public setActivePersonId = (val: string) => this.activePersonId.value = val
private resetActivePersonId = () => this.activePersonId.value = null
public apiState = this.engine.resource<{ items: TPerson[] }, number>(
async (source, abortSignal) => {
// -- NOTE: DERTY_MISTAKE 1/3 ⛔ Dont do that here!
// this.resetActivePersonId()
// --
const res = await fetch(
[
`${BASE_API_URL}/profile/search`,
'?',
[
`counter=${source}`,
'_responseDelay=3000',
`_addData=${encodeURIComponent(JSON.stringify({
items: [
{
id: 'person-id-1',
name: 'John Doe'
},
{
id: 'person-id-2',
name: 'Jane Doe'
}
]
} as { items: TPerson[] }))}`
].join('&')
].join(''),
{ signal: abortSignal }
);
return res.json();
},
this.counter
)
public inc() {
this.resetActivePersonId() // NOTE: DERTY_MISTAKE 2/3 ✅ Correct way (variant 1)
this.counter.value += 1
}
public personList = this.engine.computed(
() => !this.apiState.loading && Array.isArray(this.apiState.data?.items) && this.apiState.data?.items?.length > 0
? this.apiState.data.items
: []
)
}service.secondary.ts - подписан на успешный результат первого сервиса и выбор пользователя из списка в ответе от первого сервиса.
ts
import { AbstractService } from '@pravosleva/reactive-engine'
import { UserInfoService } from './service.firstly'
const BASE_API_URL = import.meta.env.VITE_BASE_API_URL
type TResData = {
for_person_id: string;
}
export class SecondaryService extends AbstractService {
private userInfoService = this.engine.inject(UserInfoService)
private isUserDataReceived = this.engine.computed<boolean>(
() => !this.userInfoService.apiState.loading && !!this.userInfoService.apiState.data,
'example-201:computed:isUserDataReceived'
)
private apiDeps = this.engine.computed<[boolean, string | null]>(() => [
this.isUserDataReceived.value,
this.userInfoService.activePersonId.value,
])
public apiState = this.engine.resource<TResData, [boolean, string | null]>(
async (deps, abortSignal) => {
const [isDataReceived, personId] = deps;
if (!isDataReceived) throw new Error('Waiting for user data response...');
if (!personId) throw new Error('Person id wasnt selected...');
const queryParams = new URLSearchParams({
personId: String(personId),
_responseDelay: '2000',
_addData: JSON.stringify({ for_person_id: personId } as TResData)
});
const res = await fetch(
`${BASE_API_URL}/profile/accessPolicies?${queryParams}`,
{ signal: abortSignal }
);
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
return res.json();
},
this.apiDeps,
{
name: 'example-201:resource:accessPolicies',
resetDataOnSourceChange: true,
responseValidate: (res) => {
// Пример: Бэкенд ответил 200, но ожидаемое поле отсутствует, что для нас критично
if (!res || typeof res.for_person_id !== 'string') {
return 'No access policies found for this user'; // Запишется в state.error
}
return true; // Всё отлично, запишется в state.data
}
}
)
}store.ts
ts
import { UserInfoService } from './service.firstly'
import { SecondaryService } from './service.secondary'
import { ReactiveEngine, createObserver } from '@pravosleva/reactive-engine'
const engine = new ReactiveEngine()
export const useUserInfoService = () => engine.inject(UserInfoService)
export const useSecondaryService = () => engine.inject(SecondaryService)
const observer = createObserver(engine)
export { engine, observer }