Skip to content

Instantly share code, notes, and snippets.

@zerkalica
Last active January 28, 2023 11:29
Show Gist options
  • Save zerkalica/06440939857f5b431e46c8c2a0404afc to your computer and use it in GitHub Desktop.
Save zerkalica/06440939857f5b431e46c8c2a0404afc to your computer and use it in GitHub Desktop.
React Suspense

Что нужно, что б сделать универсальный полный саспенс в реакте

  1. Заглушка при первой загрузке. Любой компонент в момент загрузки показывает опционально настраиваемую дефолтную заглушку.
  2. Автоматическая плавность перехода между перезагрузками компонента. Без isPending и подобных проверок в прикладном коде. Если первый раз уже грузили состояние, то последующая загрузка должна приводить к лёгкому приглушению и переключению на новый текст, вместо переключения на заглушку и переключению на новый текст
  3. Абстракция от асинхронности в прикладном коде. Не нужны эффекты с fetch, async/await, не нужно поднимать дозагрузку деталей стейта в угоду технической реализации работы с асинхронностью
  4. Ленивость из коробки, как, например, computed в mobx вычислится только в момент доступа к нему (что если поместить fetch в computed).
  5. Автоматизация саспенс-кэша, когда закэшированные данные, полученные через fetch и React.Suspense, очищаются при отмонтировании компонента, экономя память и уменьшая вероятность утечки
  6. Без оберток, React.Suspense в прикладном коде. Асинхронная природа данных полностью вынесена за "сцену". Это касается всех предыдущих пунктов

Способы реагировать на саспенс

1. React.Suspense

1.1. React.Suspense требует в месте вызова оборачивать дочерний компонент в Suspense, т.е. надо заранее знать бросит ли промис дочерний компонент или оборачивать вообще каждый компонент в Suspense, что жрет ресурсы и требует патчинга React.createElement

1.2. React.Suspense очищает стейт компонента во время бросания промиса. Если есть закэшеное тяжелое вычисление или фетч, то оно уничтожится после саспенса. Если создать, например, mobx модель и сохранить ее инстанс в useRef, то когда компонент засаспендится, ref уничтожится. Если в этой модели был кэш фетча, то будет новый запрос и так бесконечно. Поэтому react-loader хранит кэш глобально, в качестве ключа используя url. Такой кэш не очищается, когда становится не нужным (т.е. компонент его юзающий, отмонтировался с концами). Ключ задает человек, что может приводить к багам. Долго хранить кэш - черевато логическими багами из-за расхождения стейтов бэка и фронта и утечками памяти.

В случае вышеприведенных хотелок, приходится делать обертку над React.Suspense, которая бы хранила стейт и через контекст передавала дочернему, но хуки реакта так не сохранишь, только то, что явно знает об этом контексте, например стейт своей либы.

1.3. Отписка реактивных атомов во время перерендеров. В случае использования mobx, cellx, act, mutable, mol_wire и похожих либ, где реактивная связь образуется на атоматических подписках через глобальный контекст, а отписка (и сброс кэша свойств) автоматизируется через изменения источника (реактивной переменной) и последующего отсутствие перерендера любого потребителя (компонента) в том же тике. Например компонент A создал модель и делегировал кидающее промис свойство name компоненту B, если компонент B перезагрузится, то кэш в name сбросится (при условии что кэш не глобальный, как в react-fetch, а умный и уничтожится при отсутствии потребителей)

class User {
    static id = 1
    static name() { return myFetch('/user/123') }
}

function A() {
    return <React.Suspense children={<B name={User.name} />} />
}

function B({ name }) {
    return <div>{
        name() // Как только бросит Promise, B отпишется от name, т.к. владеет только B  
    }</div>
}

Можно попробовать каждый компонент в системе оборачивать (через патч createElement) во что-то такое:

const hocComponent = Component => {
  const Memoized = React.memo(Component)
  return props =>  <React.Suspense><Memoized {...props} /></React.Suspense>
}

2. try/catch в рендере

2.1. На сервере ведет к нетривиальному циклическому рендеру с подсчетом промисов, отказу от стримовых ssr-фишек 18го реакта.

2.2. Что отдавать в catch. Попытка в catch рисовать предыдущую vdom ноду с блендой поверх, ведет к проблемам с реконсиляцией. Внутри этой вдом ноды могут быть компоненты, которые начнут рендериться и дергать другие саспенсы. Я пробовал так: Показали заглушку, загрузили A, сохранили vdom, повторно загружаем A, AComponent в render ловит в catch Promise и рисует предыдущий vdom, оборачивая в div с блендой.

2.3. Если отдавать универсальную заглушку-пустышку во время повторной загрузки - то это ведет к лишним Content Layout Shift

2.4. Можно попробовать все оборачивать в memo, что б небыло перерендеров сабкомпонентов в catch. В условиях мутабельных объектов React.memo сломает рендер в других случаях

2.5. Еще нельзя гарантировать, что все компоненты будут хокнуты должны образом, т.к. компоненты из сторонних либ, могут асинхронно подгружены к примеру

3. useTransition

3.1 transition может сглаживать переходы только при первой загрузке и только если загрузка инициирована из экшена или эффекта, а не при первом рендере компонента с саспенсом. В примере https://codesandbox.io/s/hardcore-mcnulty-yi1x11?file=/App.js Если кнопку убрать и сделать загрузку сразу - будет лоадер, если после первой загрузки нажать кнопку, инициировав обновление данных - опять будет лоадер.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment