Снова про стейт-менеджер
@kamyshev_code



Игорь Камышев
Авиасейлс

@kamyshev_code


Оглавление
- Первый рабочий день и страдания
- Как было раньше и что изменилось
- Микрофронтенды по случайности
- Новый стейт-менеджер ☄️
- Главная фича Эффектора
@kamyshev_code



Первый рабочий день
@kamyshev_code


Как это произошло?
- Ruby
- Elixir
- CoffeeScript
Авиасейлс
уже
14 лет
Все было хорошо

@kamyshev_code


А потом пришёл бизнес

@kamyshev_code


Старая схема
Тяжесть — это надежно
@kamyshev_code



@kamyshev_code



но
есть
нюансы
No React
При рендеринге страницы на сервере, фронтенд-приложение не существовало. Вообще никакого контекста не существовало.
То есть рисовать разный UI в зависимости от пользовательского ввода было нельзя.

@kamyshev_code


Render
Нет контроля у приложения за HTML, который приходит с сервера. Приходится полностью рендерить интерфейс на клиенте ещё раз.

@kamyshev_code


AB + SSR = 😈
Любой тест с SSR — очереди и не очень удобные деплои.

@kamyshev_code


AB + SSR = 😈
@kamyshev_code



Решение 🚀
@kamyshev_code


@kamyshev_code



React SSR
Node.js
@kamyshev_code


Selene
Селена — небольшой сервис, который умеет рендерить React-приложения в HTML-строки.

@kamyshev_code


React on Server
При рендеринге страницы на сервере, фронтенд-приложение имеет весь нужный контекст.
Мы можем показывать разным пользователям разный интерфейс — по геолокации, группе в AB-тесте или знаку зодиака.
@kamyshev_code


Hydrate
Приложение точно знает какой будет HTML. Можно не перерендеривать все, а только навесить обработчики событий.
@kamyshev_code


AB + Flagr = 😻
Любой тест с SSR можно проводить на Флагре без очередей и СМС.
@kamyshev_code


Некоторые бонусы
Одной строкой
@kamyshev_code


Быстрая сборка 🤩
@kamyshev_code


Никакого легаси 🥳
@kamyshev_code


Классная архитектура 🤓
@kamyshev_code


А теперь про стейт менеджер ☄️
@kamyshev_code


Общение
Мы сделали микрофронтенды. И огребли все проблемы микрофронтендов.
В первую очередь, общение разных приложений на клиенте.
@kamyshev_code


в каждом
виджете
свой redux
Крафтовый ивент-бас

@kamyshev_code


Крафтовый ивент-бас
@kamyshev_code


const bus = new CustomEventBus({ /* ... */ });
const originChanged = bus
.take(AVIAFORM_CHANGED)
.map(({ data }) => data.origin);
// ... 🤔
const bus$ = /* ... */;
const origin$ = bus$.pipe(
filter(({ type }) => type === AVIAFORM_CHANGED),
map(({ data }) => data.origin),
);
EventBus ➔ RxJS
Поняли, что написали свой Rx и выкинули его, заменив на настоящий Rx.
@kamyshev_code



RxJS 💩
И тут началось
@kamyshev_code


Почему нам не подошел Rx
@kamyshev_code


Все — событие
(нет)
@kamyshev_code


события
эффекты
состояние

Шаг 1: делать
@kamyshev_code



@kamyshev_code


Шаг 1: думать
Варианты
@kamyshev_code


Использовать только RxJS
✅ Ничего не нужно менять
⛔️ Нужно продолжать страдать
@kamyshev_code


Использовать только Redux
✅ Знакомый старый друг
⛔️ Очень заточен на единственный стор
⛔️ Переиспользовать кусочки логики между изолированными приложениями больно
@kamyshev_code


MobX
✅ Мультисторы — кайф
✅ Супер-удобный интероп с RxJS
⛔️ Работа с SSR
⛔️ Все еще тяжеловат (60+ кб)
⛔️ Фанатично императивный, бывает сложно следить за потоком данных.
@kamyshev_code


Effector
✅ Типизация
✅ Нет проблем с SSR + дата-фетчингом
✅ Наш, русский!
⛔️ Чтобы прочитать сорцы нужно иметь степень по компьютер-сайнс
⛔️ Экосистема не слишком богата
⛔️ Фанатично декларативный, бывает сложно писать
@kamyshev_code


Мы рискнули и выбрали Эффектор
@kamyshev_code


события
эффекты
состояние
// новое реактивное значение
const $value = createStore('')
// новое событи
const smthHappened = createEvent()
// новый сайд-эффект
const writeToLocalStoreFx = createEffect(({ key, vale }) => localStorage.set(key, value))
// создание связей
sample({
source: $value,
clock: smthHappened,
fn: (value) => ({ key: 'SOME_KEY', value }),
target: writeToLocalStoreFx,
})
@kamyshev_code


const $login = createStore('')
const $password = createStore('')
const loginFormSubmit = createEvent()
const $userName = createStore(null)
const loginFx = createEffect(async ({ login, password }) => {...})
sample({
source: {
login: $login,
password: $password,
},
clock: loginFormSubmit,
target: loginFx,
})
sample({
clock: loginFx.doneData,
fn: (user) => user.name,
target: $userName,
})
@kamyshev_code


Фича сюрприз!
@kamyshev_code


Как писать тесты
без страданий
@kamyshev_code


Fork API
@kamyshev_code


Что такое fork
// https://share.effector.dev/SEQBqetV
const $counter = createStore(0)
const increase = createEvent()
$counter.on(increase, count => count + 1)
const scope = fork()
console.log('ORIGINAL', $counter.getState()) // 0
console.log('SCOPE', scope.getState($counter)) // 0
await allSettled(increase, { scope })
console.log('ORIGINAL', $counter.getState()) // 0
console.log('SCOPE', scope.getState($counter)) // 1
@kamyshev_code


allSettled
который решает проблемы
@kamyshev_code


Что такое allSettled
// https://share.effector.dev/3HsxG6u7
const $counter = createStore(0)
const asyncIncreaseFx = createEffect(() => wait(1000))
const complexFx = createEffect(() => {
// DO NOT AWAIT!!!
asyncIncreaseFx()
})
$counter.on(asyncIncreaseFx.done, count => count + 1)
const scope = fork()
await allSettled(complexFx, { scope })
console.log(scope.getState($counter)) // 1
@kamyshev_code


Подмена значений и хендлеров
@kamyshev_code


Без грязи 💩
// https://share.effector.dev/7euTAEJv
const $counter = createStore(0)
const asyncIncreaseFx = createEffect(async () => {
console.log('REAL')
await wait(1000)
})
$counter.on(asyncIncreaseFx.done, count => count + 1)
const scope = fork({
handlers: [
[asyncIncreaseFx, () => console.log('FAKE')]
]
})
await allSettled(asyncIncreaseFx, {scope}) // FAKE
@kamyshev_code


Без грязи 💩
// https://share.effector.dev/WhINUSBd
const $counter = createStore(1000)
const scope = fork({
values: [
[$counter, 1]
]
})
console.log($counter.getState()) // 1000
console.log(scope.getState($counter)) // 1
@kamyshev_code


В итоге тесты получаются удобные
@kamyshev_code


describe('analyticsService', () => {
test('sends events for inited service instantly', async () => {
const sendMock = jest.fn();
const scope = fork({
// Пусть аналитика уже инициализирована
values: [[$inited, true]],
// Подменим эффект отправки событий
handlers: [[sendAnalyticsEventFx, sendMock]],
});
await allSettled(sendEvent, { scope, params: FAKE_EVENT_1 });
expect(sendMock).toHaveBeenCalledTimes(1);
expect(sendMock).toBeCalledWith(null, FAKE_EVENT_1);
await allSettled(sendEvent, { scope, params: FAKE_EVENT_2 });
expect(sendMock).toHaveBeenCalledTimes(2);
expect(sendMock).toBeCalledWith(null, FAKE_EVENT_2);
});
})
@kamyshev_code


Кстати, для SSR можно использовать тот же механизм
@kamyshev_code


async function renderApp({ query, lang, cookie }) {
const scope = fork(root, {
values: [
[$query, query],
[$language, lang],
],
handlers: new Map([
[readCookieFx, (key) => cookie[key] ?? null],
]),
});
await allSettled(appInited, { scope });
const element = createElement(
'div',
{ 'data-scope': JSON.stringify(serialize(scope)) },
createElement(
Provider,
{ value: scope },
createElement(App);
)
)
return renderToString(element);
}
Ну штош
это был наш путь ☄️
спасибо 👨💻
github.com/sponsors/effector
twitter.com/EffectorJS
t.me/kamyshev_code
twitter.com/kamyshev_code
blog.kamyshev.me/tag/effector/
HolyJS Moscow 2021 Kamyshev
By Igor Kamyshev
HolyJS Moscow 2021 Kamyshev
- 702