Снова про стейт-менеджер
@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
- 629