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

@kamyshev_code

Игорь Камышев

Авиасейлс

@kamyshev_code

Оглавление

  1. Первый рабочий день и страдания
  2. Как было раньше и что изменилось
  3. Микрофронтенды по случайности
  4. Новый стейт-менеджер ☄️
  5. Главная фича Эффектора
@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/