Всем привет! Меня зовут Слава, сейчас работаю в Samsung Next, ранее в Yandex, уже 4 года занимаюсь активным созданием своих библиотек в Open Source, и сейчас я расскажу что удивило и продолжает удивлять меня уже полтора года!

Позапрошлым летом, я за несколько недель написал простую библиотеку, в тот момент я увлекался внутренним устройством React и решил поиграть с интеграцией в его внутреннюю кухню.

Я научился подменять логику хуков React) И обнаружил что это уже используется в девелоперских тулзах при сборе информации о вызванных хуках и даже более того, Preact в compat режиме тоже предоставляет эквивалентные возможности.

В этот момент я уже работал в Samsung Next и активно участвовал в формировании архитектуры проекта и меня восхищало решение принятое на проекте, о том, что вместо DI мы будет использовать Service Provider. Это обозначало, что теперь имея функцию фабрику с логикой или класс, можно из любого места приложения получить методы нужной логики и данные состояния.

И трам-парам как по-фолшебству, как в новогоднюю ночь, проявился код. Я подумал ведь можно же управлять состоянием приложения на React используя всего один дополнительный хук!

Идея была такой, а что если представить что функция хука это Service Provider, причем поставляет она очень интересный сервис. Он может содержать реактивную логику через useEffect, может кешировать нагруженные вычисления через useMemo, а благодаря useState мы определяем его состояние!

Посмотрите как красиво смотрится такой сервис:

const useCounter = () => {
    const [counter, setCounter] = useState(0)
    const inc = () => setCounter(v => v + 1)

    return {
        counter,
        inc
    }
}

Ведь теперь это не просто хук который будет хранить свое состояние в React компоненте. Это сервис, который имеет своё собственное, изолированное состояние и будет доступен из любого места в коде, а все кто его используют будут реагировать на его изменения. React компоненты будут обновлять соответсвующие им области веб страницы при изменении данных внутри используемых сервисов.

При более глубоком рассмотрении можно заменить что это уже не просто описание логики и данных используя стандартные хуки React, это объекты, это инкапсулирование данных и предоставление интерфейса для операций с ними.

А вот так будет смотреться синхронизация данных состояния и локального хранилища браузера.

const useDark = () => {
    const [dark, setDark] = useState(
        localStorage.getItem('dark') === 'on'
    )

    useEffect(() => {
        localStorage.setItem('dark', dark ? 'on' : 'off')
    }, [dark])

    return {
        dark,
        setDark
    }
}

И здесь мы инкапсулировали даже реактивное выражение! При каждом изменении значения dark, будет производиться его синхронизация. Как вы уже заметили это переключалка ночной темы, теперь благодаря Service Provider можно использовать это состояние в каком угодно компоненте и все они будут использовать одно состояние.

Все оказалось предельно просто и понятно, и теперь благодаря всего одному хуку, можно создать прикольную штуку)

const Counter = () => {
  const { counter } = useBetween(useCounter);

  return <p>{counter}</p>;
}

const Button = () => {
  const { inc } = useBetween(useCounter);

  return (
    <button onClick={inc}>+</button>
  )
}

const App = () => (
  <>
    <Counter />
    <Button />
    <Counter />
    <Button />
  </>
)

Глянуть этот код в работе можно на Codesandbox по этой ссылке.

Мы создали минимальное React приложение в котором между четырьмя компонентами делится один счетчик.

Правда! Выглядет реально круто! То есть мы такие взяли и внедрили общее состояние для двух разных React компонентов. Никакого кода в новом стандарте, только хуки.

Хук становится идентификатором сервиса, а сам сервис создается только при вызове useBetween. Иными словами теперь мы можем пользоваться результатом вызова хука между компонентами.

Так же очень подкупает тот факт что по сути ничего не нужно учить нового, ты просто используешь хуки между компонентами и хранишь там состояние о пользователе, покупках, да всё что угодно.

Но к сожалению не все хуки React можно переиспользовать, например useContext оказался крепким орешком. Внутри React нет возможности подписаться на контекст вне React компонента, а именно там как раз и создаётся общее состояние, за пределами компонента нет и контекста.

Спустя полтора года библиотека продолжает развиваться, растет количество использующих её проектов. И я этому правда очень удивлён, так как я совершенно не занимаюсь популяризацией) Я люблю писать код и делиться с людьми классным кодом!

В итоге получилось элегантное решение для написания небольших и среднего размера проектов. Для больших проектов я всё же порекомендовал бы брать что-то более популярное, так как будет довольно сложно найти большое количество специалистов в use-between)

https://www.npmjs.com/package/use-between
https://github.com/betula/use-between

Комментарии (28)


  1. LabEG
    18.01.2023 16:44

    Вот только мемоизация любого из компонента убьет все ваше открытие. Кроме того оно не работает вверх по дереву. Используйте контексты или MobX и не страдайте.


    1. betula Автор
      18.01.2023 17:46
      +1

      Это не так. Каждый компонент подписывается на изменения стейта который в нём используется. Под копотом обычный state manager.


    1. DarthVictor
      18.01.2023 18:24

      Используйте контексты или MobX и не страдайте.

      Не надо использовать контексты для чего-то изменяемого. Используйте useSyncExternalStorage.


      1. markelov69
        18.01.2023 18:31

        Не надо использовать контексты для чего-то изменяемого. Используйте useSyncExternalStorage.

        Context только для того чтобы шарить ссылку на состояние верхнеуровневое(экземпляр класса) между дочерними компонентами.

        export const CardItem = observer(() => {
            const [state] = useState(() => new CardState());
        
            return (
                <CardStateContext.Provider value={state}>
                    <CardHeader />
                    <CardBody />
                    <CardFooter />
                </CardStateContext.Provider>
            )
        })
        
        const CardHeader = observer(() => {
          const state = useContext(CardStateContext);
        
          return (
            <div className={styles.card_header}>
              //...
              <CardMenu />
            </div>
          )
        })
        
        const CardMenu = observer(() => {
          const state = useContext(CardStateContext);
        
          return (
            <div className={styles.card_menu}>
              //...      
            </div>
          )
        })

        В таком духе.


  1. markelov69
    18.01.2023 17:05

    Каким только извратом не страдают. Нет бы просто использовать MobX и context(только по назначению) и вообще не о чем не парится и не страдать.


    1. betula Автор
      18.01.2023 17:53
      -1

      Совсем разные задачи. Здесь людям предоставляется удобный дизайн описания кода, при котором переиспользуется знание хуков. В случае с mobx, что бы написать хороший код придётся выучить много нового. Порог вхождения совершенно разный.


      1. markelov69
        18.01.2023 18:05

        Вообще ничего учить не надо. 10 минут документации и 10 минут экспериментов и готово.

        class State {
          counter = 1;
          
          constructor() {
            makeAutoObservable(this);
          }  
        }
        
        const state = new State();
        setInterval(() => { state.counter++; }, 1000);
        
        const Component = observer(() => {
          return <div>counter value: {state.counter}</div>
        });

        Вообще элементарщина и всё работает как ожидается.


        1. betula Автор
          18.01.2023 18:35
          +1

          Я тоже знаю mobx. И считаю его вполне интересным решением, правда его идею с "makeAutoObservable" считаю не очень удачной, да и прокси вместо реальных данных мне тоже не нравятся. А так же остаётся запретить "autorun" на проекте и научить всех проектировать нормальное ООП. Но я очень уважаю mobx, они отличные ребята и держат марку.


          1. markelov69
            18.01.2023 18:45

            да и прокси вместо реальных данных мне тоже не нравятся

            import { configure } from "mobx"
            
            configure({
                useProxies: "never"
            })

            правда его идею с "makeAutoObservable" считаю не очень удачной

            Я тоже по началу так считал, но теперь мне наоборот нравится, кода меньше и код чище. Но я использую свою функцию вместо makeAutoObservable, которая делает тоже самое, только c deep: false и вместо observable.ref => observable.shallow, 99.9% мне нужно вглубь реактивный объект/массив т.к. всё равно все данные с сервера фетчатся и просто подменяются.

            А так же остаётся запретить "autorun" на проекте и научить всех проектировать нормальное ООП

            Как 2 пальца об асфальт


  1. petrov_engineer
    18.01.2023 17:07

    __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED


    1. betula Автор
      18.01.2023 17:50
      -1

      Ага. Но меня такое не останавливает) Уже с конца 16-ой версии Реакта никаких изменений. Использование внутреннего API даёт крутые возможности, его скорее сделают задокументированным чем удалят без предоставления альтернатив.


      1. yroman
        19.01.2023 00:32

        The most important addition in React 18 is something we hope you never have to think about: concurrency.

        Ну прям даже не знаю, что-то сомнительно, что вот никаких изменений. Just for fun оно хорошо, конечно, но для чего-то серьезного вряд ли применимо.


  1. serginho
    18.01.2023 18:12

    То же самое на Mobx. Сравните читабельность.


    1. betula Автор
      18.01.2023 19:00

      Мне больше всех нравятся минималистичные варианты типа Jotai, на работе пишу на Mobx, но для своих проектов использую личные разработки (например Remini), в какой-то момент становиться интереснее написать то что самому себе хочется, чем мириться с проблемами любого существующего стейт менеджера.

      Адептам Mobx я противостоять не вижу смысла, как и сражаться с другими командами, делающими по-своему отличные решения, напротив мы все развиваем одну отрасль.

      use-between же о другом, и лично я ищу тех кому будет интересно писать свой проект в стиле хуков! И сердечно предлагаю попробовать свежее решение.


      1. markelov69
        18.01.2023 19:22
        -1

        я ищу тех кому будет интересно писать свой проект в стиле хуков!

        Т.е. проекты сразу же обреченные быть write only и неподдерживаемыми спустя уже пол года разработки. Зашибись)

        Мне больше всех нравятся минималистичные варианты типа Jotai,

        Не используется getters/setters - смешно, на помойку. Максимально убогий интерфейс взаимодействия через setLala(newValue) нелепость.

        но для своих проектов использую личные разработки (например Remini)

        Такой же бесполезный хлам.

        мириться с проблемами любого существующего стейт менеджера.

        А может просто перестать считать проблемами, "проблемы" притянутые за уши и которые в реальной жизни ни на что не влияют и никак не ощущаются.

        А вообще в чем сакральный смыл намеренно говнокодить и вообще писать лишний код?


        1. betula Автор
          18.01.2023 19:36

          Хм. Удивительно грубая речь. Думаю вопрос риторический. Могу немного перефразировать, следует вкладывать смысл во всё что мы пишем, а не только в код. Не писать глупостей и лишнего)


          1. markelov69
            18.01.2023 20:28
            -2

            Думаю вопрос риторический

            Ну не совсем. Использовать на проекте управление состоянием в виде хуков реакта, redux и прочей шелухи - это гарантированно запоротый write-only проект.

            Единственный стейт менеджер для реакта который позволяет писать человеческий код это MobX. Т.к. там getters/setters, мутабильность, автоматическая подписка/отписка, 0% лишнего кода(не считая makeAutoObservable и observable, но любой уважающий себя разработчик и это не пишет, т.к. давно написал плагин/трансформер кода и все компоненты на этапе сборки заворачиваются в observable, а классы в которых нет вызова makeAutoObservable в конструкторе, превращаются в классы с этим вызовом) и другие приятности например функция when.

            P.S. Я бы рад отвечать быстрее, но из-за всяких недалеких, обиженных жизнью, которые заминусовали карму из-за того, что мое мнение не сходится с их мнением, приходится писать не чаще 1 раза в час. Понятное дело что серой массе комфортно находится именно в обществе серой массы, но что тогда делать тем, кто мыслит глубже и дальше?


            1. betula Автор
              18.01.2023 20:49

              Жму руку. Приятно выйти на дружественную ноту!

              Классно написал про MobX, я тоже писал свой плагин на babel, что бы автоматически оборачивать компоненты в observer) нашелся бы кто-нибудь кто в open source оформил бы такой, да что бы ещё работал на SWC, вообще б цены не было!


              1. Alexandroppolus
                18.01.2023 21:37
                +1

                плагин на babel

                Это и без бабеля можно, через допилку внутренностей Реакта.

                Например, так сделано в signal (упрощенная копия mobx) - https://github.com/preactjs/signals/blob/main/packages/react/src/index.ts#L189


            1. serginho
              19.01.2023 13:56

              но любой уважающий себя разработчик и это не пишет, т.к. давно написал плагин/трансформер кода и ..

              Уважающий себя, но не уважающий других. Я предпочитаю использовать по минимуму нестандартные плагины, т.к. любому новому разработчику придется вникать, почему у вас все это работает без обычных функций инициализации. И врядли вы прям уж так досконально пишите документацию к вашим плагинам. Добавить одну строчку в класс это не так страшно.


              1. markelov69
                19.01.2023 14:03

                Так в readme.md просто написано - "все компоненты реакта автоматически заворачиваются в observer(). См. path/to/plugin.ts"


  1. StiPAFk
    18.01.2023 20:45

    спасибо за статью, чуть ранее писал про удобное использование контекстов статью которая решает все эти проблемы.
    В целом ваш способ позволяет пошарить состояние из кастомного хука. но это не решает ни как проблему не явно DI. так как с таким подходом может быть хук в хуке внутри которого хук. и вы заколебаетесь искать виновника который рендерит ваш компонент)


    1. StiPAFk
      18.01.2023 20:46
      -1

      p.s сверху хейтеры у которых есть только redux/mobx и асинхронные сторы :)

      у меня под статьёй так же насрали


    1. betula Автор
      18.01.2023 22:09

      Спасибо за добрые слова!)

      Да. Есть такое дело, конечно “use-between” не защитит.

      Современная бизнес ориентированная среда фронтенда продвигает идею, что не нужно решать проблем производительности до их появления. А архитекторы же напротив будут требовать закладывать как можно более надежный фундамент и контролировать код в течении всей его жизни с первых дней.

      Реальный пример из жизни, я работал над большим проектом с redux, где повсеместно в компонентах использовался “useSelector”, мейнтейнер так беспокоился о производительности функций компонентов, что все вычисления вынес внутрь “useSelector”. А в итоге сработал обратый эффект, так как при изменении любого значения внутри стора даже самого незначительного запускались пересчёты всех “useSelector” из отображенных компонентов. В какой-то момент это стало заметно.

      Для меня use-between это так же и уход от единого стора и возможность делить данные так, что бы пересчётов было как можно меньше. Что бы сторы были как можно проще и вообще я бы ввел ограничение на размер файла в один-два экрана для 95% случаев), что бы код было проще читать.

      Грамотное использование React.memo, маленькие сторы, слежение за эффектами и как можно более “плоская” структура, так как чем больше вложений тем сложнее размотать потом код.

      Хорошо что всё эти рекомендации применимы к Реакт приложению и без use-between!)


      1. Alexandroppolus
        18.01.2023 22:33

        мейнтейнер так беспокоился о производительности функций компонентов, что все вычисления вынес внутрь “useSelector”. А в итоге сработал обратый эффект, так как при изменении любого значения внутри стора даже самого незначительного запускались пересчёты всех “useSelector” из отображенных компонентов. В какой-то момент это стало заметно.

        Там палка о двух концах. С одной стороны, иногда полезно вычислять селектором, например, в массиве объектов проверить, есть ли объекты с некоторым свойством. Такой селектор возвращает true/false, и перерендер компонента будет далеко не так часто после изменения массива, как если бы мы просто заселектили массив и посчитали уже в компоненте. А с другой, если есть редко изменяемые данные, но по которым отрабатывают какие-нибудь числодробилки, то тут да, лишних вычислений будет много. Впрочем, этот последний пункт легко исправляется библиотекой reselect


  1. andres_kovalev
    19.01.2023 00:30

    Это уже не первая библиотека, которая прячет за "хуками" использование глобальной переменой спрятанной в самой библиотеке (`instances`). Примерно так же поступает бОльшая половина библиотек "убийц редакса" (я не поклонник редакса, просто отметил).

    Вопрос из зала - как Вы это тестируете?


    1. betula Автор
      19.01.2023 04:40

      Хм. Тесты находятся на Github в репозитории с кодом.


      1. andres_kovalev
        19.01.2023 11:54

        ```

        export const clear = () => instances.clear()

        ```

        =/