Вашему вниманию представляется react-redux-cache (RRC) - легковесная библиотека для загрузки и кэширования данных в React приложениях, которая поддерживает нормализацию, в отличие от React Query и RTK Query, при этом имеет похожий, но очень простой интерфейс. Построена на базе Redux, покрыта тестами, полностью типизирована и написана на Typescript.

RRC можно рассматривать как ApolloClient для протоколов, отличных от GraphQL (хотя теоретически и для него тоже), но с хранилищем Redux - с возможностью писать собственные селекторы (selector), экшены (action) и редьюсеры (reducer), имея полный контроль над кэшированным состоянием.

Зачем?

Далее пойдет сравнение с имеющимися библиотеками для управления запросами и состоянием. Почему вообще стоит пользоваться библиотеками для этого, а не писать все вручную с помощью useEffect / redux-saga и тп - оставим эту тему для других статей.

  • Полный контроль над хранилищем не только дает больше возможностей, упрощает отладку и написание кода, но и позволяет городить меньше костылей если задача выходит за рамки типичного hello world из документации, не тратя огромное время на страдания с очень сомнительными интерфейсами библиотек и чтением огромных исходников.

  • Redux это отличный - простой и проверенный инструмент для хранения “медленных” данных, то есть тех, что не требуют обновления на каждый кадр экрана / каждое нажатие клавиши пользователем. Порог входа для тех, кто знаком с библиотекой - минимальный. Экосистема предлагает удобную отладку и множетсво готовых решений, таких как хранение состояния на диске (redux-persist). Написан в функциональном стиле.

  • Нормализация - это лучший способ поддерживать согласованное состояние приложения между различными экранами, сокращает количество запросов и без проблем позволяет сразу отображать кэшированные данные при навигации, что значительно улучшает пользовательский опыт. А аналогов, поддерживающих нормализацию, практически нет - ApolloClient поддерживает только протокол GraphQL, и сделан в весьма сомнительном, переусложненном ООП стиле.

  • Легковесность, как размера библиотеки, так и ее интерфейса - еще одно преимущество. Чем проще, тем лучше - главное правило инженера, и данной конкретной библиотеки.

Краткое сравнение библиотек в таблице:

React Query

Apollo Client

RTK-Query

RRC

Полный доступ хранилищу

-

-

+-

+

Поддержка REST

+

-

+

+

Нормализация

-

+

-

+

Бесконечная пагинация

+

+

-

+

Не переусложнена

+

-

-

+

Популярность

+

+

-

-

Почему только React?

Поддержка всевозможных UI библиотек кроме самой популярной (используемой в том числе в React Native) - усложнение, на которое я пока не готов.

Примеры

Для запуска примеров из папки /example используйте npm run example. Доступны три примера:

  • С нормализацией (рекомендуется).

  • Без нормализации.

  • Без нормализации, оптимизированный.

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

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

  • запросы постоянно отправляются, даже если данные все еще достаточно свежие.

Пример состояния redux с нормализацией

{
  entities: {
    // Каждый тип имеет свой словарь сущностей, хранящихся по id.
    users: {
      "0": {id: 0, bankId: "0", name: "User 0 *"},
      "1": {id: 1, bankId: "1", name: "User 1 *"},
      "2": {id: 2, bankId: "2", name: "User 2"},
      "3": {id: 3, bankId: "3", name: "User 3"}
    },
    banks: {
      "0": {id: "0", name: "Bank 0"},
      "1": {id: "1", name: "Bank 1"},
      "2": {id: "2", name: "Bank 2"},
      "3": {id: "3", name: "Bank 3"}
    }
  },
  queries: {
    // Каждый запрос имеет свой словарь состояний, хранящихся по ключу кэша, генерируемого из параметров запроса
    getUser: {
      "2": {loading: false, error: undefined, result: 2, params: 2},
      "3": {loading: true, params: 3}
    },
    getUsers: {
      // Пример состояния с пагинацией под переопределенным ключом кэша (см. далее в пункте про пагинацию)
      "all-pages": {
        loading: false,
        result: {items: [0,1,2], page: 1},
        params: {page: 1}
      }
    }
  },
  mutations: {
    // Каждая мутация так же имеет свое состояния
    updateUser: {
      loading: false,
      result: 1,
      params: {id: 1, name: "User 1 *"}
    } 
  }
}

Пример состояния redux без нормализации

{
  // Словарь сущностей используется только для нормализации, и здесь пуст
  entities: {},
  queries: {
    // Каждый запрос имеет свой словарь состояний, хранящихся по ключу кэша, генерируемого из параметров запроса
    getUser: {
      "2": {
        loading: false,
        error: undefined,
        result: {id: 2, bank: {id: "2", name: "Bank 2"}, name: "User 2"},
        params: 2
      },
      "3": {loading: true, params: 3}
    },
    getUsers: {
      // Пример состояния с пагинацией под переопределенным ключом кэша (см. далее в пункте про пагинацию)
      "all-pages": {
        loading: false,
        result: {
          items: [
            {id: 0, bank: {id: "0", name: "Bank 0"}, name: "User 0 *"},
            {id: 1, bank: {id: "1", name: "Bank 1"}, name: "User 1 *"},
            {id: 2, bank: {id: "2", name: "Bank 2"}, name: "User 2"}
          ],
          page: 1
        },
        params: {page: 1}
      }
    }
  },
  mutations: {
    // Каждая мутация так же имеет свое состояния
    updateUser: {
      loading: false,
      result: {id: 1, bank: {id: "1", name: "Bank 1"}, name: "User 1 *"},
      params: {id: 1, name: "User 1 *"}
    } 
  }
}

Установка

react, redux и react-redux являются peer-зависимостями.

npm add react-redux-cache react redux react-redux

Инициализация

Единственная функция, которую нужно импортировать — это createCache, которая создаёт полностью типизированные редьюсер, хуки, экшены, селекторы и утилиты для использования в приложении. Можно создать столько кэшей, сколько нужно, но учтите, что нормализация не переиспользуется между ними. Все типы, запросы и мутации должны быть переданы при инициализации кэша для корректной типизации.

cache.ts

export const {
  cache,
  reducer,
  hooks: {useClient, useMutation, useQuery},
} = createCache({
  // Используется как префикс для экшенов и в селекторе выбора состояния кэша из состояния redux
  name: 'cache',
  // Словарь соответствия нормализованных сущностей их типам TS
  // Можно оставить пустым, если нормализация не нужна
  typenames: {
    users: {} as User, // здесь сущности `users` будут иметь тип `User`
    banks: {} as Bank,
  },
  queries: {
    getUsers: { query: getUsers },
    getUser: { query: getUser },
  },
  mutations: {
    updateUser: { mutation: updateUser },
    removeUser: { mutation: removeUser },
  },
})

Для нормализации требуется две вещи:

  • Задать typenames при создании кэша - список всех сущностей и соответствующие им типы TS.

  • Возвращать из функций query или mutation объект, содержащий помимо поля result данные следующего типа:

type EntityChanges<T extends Typenames> = {  
  // Сущности, что будут объединены с имеющимися в кэше
  merge?: PartialEntitiesMap<T>
  // Сущности что заменят имеющиеся в кэше
  replace?: Partial<EntitiesMap<T>>
  // Идентификаторы сущностей, что будут удалены из кэша
  remove?: EntityIds<T>
  // Алиас для `merge` для поддержки библиотеки normalizr
  entities?: EntityChanges<T>['merge']
}

store.ts

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

const store = configureStore({
  reducer: {
    [cache.name]: reducer,
    ...
  }
})

api.ts

Результат запроса должен быть типа QueryResponse, результат мутации — типа MutationResponse. Для нормализации в этом примере используется пакет normalizr, но можно использовать другие инструменты, если результат запроса соответствует нужному типу. В идеале - бэкэнд возвращает уже нормализованные данные.

По части race condition:

  • Для query используется throttling - пока идет запрос с определенными параметрами, другие с теми же параметрами отменяются.

  • Для мутаций используется debounce - каждая следующая мутация отменяет предыдущую, если та еще не завершилась. Для этого вторым параметром в мутации передается abortController.signal.

// Пример запроса с нормализацией (рекомендуется)

export const getUser = async (id: number) => {
  const result = await ...
  
  const normalizedResult: {
    // result - id пользователя
    result: number
    // entities содержат все нормализованные сущности
    entities: {
      users: Record<number, User>
      banks: Record<string, Bank>
    }
  } = normalize(result, getUserSchema)

  return normalizedResult
}

// Пример запроса без нормализации

export const getBank = (id: string) => {
  const result: Bank = ...
  return {result}
}

// Пример мутации с нормализацией

export const removeUser = async (id: number, abortSignal: AbortSignal) => {
  await ...
  return {
    remove: { users: [id] }, // result не задан, но указан id пользователя, что должен быть удален из кэша
  }
}

UserScreen.tsx

export const UserScreen = () => {
  const {id} = useParams()

  // useQuery подключается к состоянию redux, и если пользователь с таким id уже закэширован,
  // запрос не будет выполнен (по умолчанию политика кэширования 'cache-first')
  const [{result: userId, loading, error}] = useQuery({
    query: 'getUser',
    params: Number(id),
  })

  const [updateUser, {loading: updatingUser}] = useMutation({
    mutation: 'updateUser',
  })

  // Этот hook возвращает сущности с правильными типами — User и Bank
  const user = useSelectEntityById(userId, 'users')
  const bank = useSelectEntityById(user?.bankId, 'banks')

  if (loading) {
    return ...
  }

  return ...
}

Продвинутые возможности

Расширенная политика кэширования

По умолчанию политика cache-first не загружает данные, если результат уже закэширован, но иногда она не может определить, что данные уже присутствуют в ответе другого запроса или нормализованном кэше. В этом случае можно использовать параметр skip:

export const UserScreen = () => {
  ...

  const user = useSelectEntityById(userId, 'users')

  const [{loading, error}] = useQuery({
    query: 'getUser',
    params: userId,
    skip: !!user // Пропускаем запрос, если пользователь уже закэширован ранее, например, запросом getUsers
  })

  ...
}

Мы можем дополнительно проверить, достаточно ли полный объект, или, например, время его последнего обновления:

skip: !!user && isFullUser(user)

Другой подход — установить skip: true и вручную запускать запрос, когда это необходимо:

export const UserScreen = () => {
  const screenIsVisible = useScreenIsVisible()

  const [{result, loading, error}, fetchUser] = useQuery({
    query: 'getUser',
    params: userId,
    skip: true
  })

  useEffect(() => {
    if (screenIsVisible) {
      fetchUser()
    }
  }, [screenIsVisible])

  ...
}

Бесконечная прокрутка с пагинацией

Вот пример конфигурации запроса getUsers с поддержкой бесконечной пагинации - фичи, недоступной в RTK-Query (facepalm). Полную реализацию можно найти в папке /example.

// createCache

...
} = createCache({
  ...
  queries: {
    getUsers: {
      query: getUsers,
      getCacheKey: () => 'all-pages', // Для всех страниц используется единый ключ кэша
      mergeResults: (oldResult, {result: newResult}) => {
        if (!oldResult || newResult.page === 1) {
          return newResult
        }
        if (newResult.page === oldResult.page + 1) {
          return {
            ...newResult,
            items: [...oldResult.items, ...newResult.items],
          }
        }
        return oldResult
      },
    },
  },
  ...
})

// Компонент

export const GetUsersScreen = () => {
  const [{result: usersResult, loading, error, params}, fetchUsers] = useQuery({
    query: 'getUsers',
    params: 1 // страница
  })

  const refreshing = loading && params === 1
  const loadingNextPage = loading && !refreshing

  const onRefresh = () => fetchUsers()

  const onLoadNextPage = () => {
    const lastLoadedPage = usersResult?.page ?? 0
    fetchUsers({
      query: 'getUsers',
      params: lastLoadedPage + 1,
    })
  }

  const renderUser = (userId: number) => (
    <UserRow key={userId} userId={userId}>
  )

  ...

  return (
    <div>
      {refreshing && <div className="spinner" />}
      {usersResult?.items.map(renderUser)}
      <button onClick={onRefresh}>Refresh</button>
      {loadingNextPage ? (
        <div className="spinner" />
      ) : (
        <button onClick={onLoadNextPage}>Load next page</button>
      )}
    </div>
  )
}

redux-persist

Вот простейшая конфигурация redux-persist:

// Удаляет `loading` и `error` из сохраняемого состояния
function stringifyReplacer(key: string, value: unknown) {
  return key === 'loading' || key === 'error' ? undefined : value
}

const persistedReducer = persistReducer(
  {
    key: 'cache',
    storage,
    whitelist: ['entities', 'queries'], // Cостояние мутаций не сохраняем
    throttle: 1000, // ms
    serialize: (value: unknown) => JSON.stringify(value, stringifyReplacer),
  },
  cacheReducer
)

Заключение

Хоть проект и находится на стадии развития, но уже готов к использованию. Конструктивная критика и квалифицированная помощь приветствуется.

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


  1. clerik_r
    14.09.2024 12:30

    А что насчет debouce?
    Что насчет race condition?

    А почему просто не использовать классику?

    import { makeAutpObservable } from "mobx";
    
    class SomeState {
      fetching = false;
      error = null;
      item: IItem = null;
    
      constuctor() {
        makeAutpObservable(this);
      }
      
      fetchData = async () => {
        this.fetching = true;
        const ah = asyncHelpers(this.fetchData);
        // Debounce + no race condition 300ms
        if (!await ah.debouce(300))  return;
      
        try {
          const item = await new ApiReq(`GET /api/v1/item/${this.itemId}`)
                                            .withCahe(60) // кэш 60сек
                                            // для отмены запросов
                                            .withAbort(ah.abortControllersArray)
                                            .send()
          // race condition check
          if (!ah.stillActual()) return;
    
          const itemComments = await new ApiReq(`GET /api/v1/item-comments/${item.commentsId}`)
                                            .withCahe(60) // кэш 60сек
                                            // для отмены запросов
                                            .withAbort(ah.abortControllersArray)
                                            .send()
          // race condition check
          if (!ah.stillActual()) return;
    
          const data:IItem = {
            ...item,
            comments: itemComments
          }
    
          this.item = data;
          this.error = null;
        } catch (e) {
          // race condition check
          if (!ah.stillActual()) return;
    
          this.error = e;
        } finally {
          // race condition check
          if (!ah.stillActual()) return;
    
          this.fetching = false;
        }
      }
    }

    Ну и дальше в компоненте

    const MyList = observer(() => {
      useState(() => { someState.fetchData(); });
      if (someState.fetching) return <Spinner />
    
      return (
        <div className={styles.list_container}">
          {someState.map(item => <div className={styles.list_item}>...</div>)}
        </div>
      )
    });


    1. gen1lee Автор
      14.09.2024 12:30

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

      Что ж, по порядку:

      • Здесь использована магия (не все любят магию) mobx с ООП (многие считают эту парадигму неудачной), обертками компонент (увеличение vdom, стектрейса). Вынесем это за скобки ибо холивар разводить не хотелось бы.

      • Использована какая то неизвестная мне функция asyncHelpers для debounce и отмены запросов.

      • Использован неизвестный мне класс ApiReq (опять ООП) в тч для кэширования запросов. Причем в данной реализации нет возможности принудительно запустить обновление данных, например через Pull to refresh - данные минуту всегда будут приходить из кэша. Неприятный баг UX.

      • fetching каждый раз переходит в true, а в конце в false, тем самым показывая на короткое время спинер даже если данные уже закэшированы - UI баг. Возможно потребуется серьезный рефакторинг чтобы это исправить, если не полный отказ от данной архитектуры. Тут нужно смотреть на реализацию ApiReq.

      • Отсутствует нормализация - плюс множество проблем консистенстности данных в приложении, и увеличивается количеств запросов к серверу. Возможно придется полностью переписать архитектуру, чтобы ее прикрутить.

      • Данные item можно показать еще пока не погрузились комментарии, но не в данном примере. Потребуется рефакторинг.

      • Легко ли прикрутить, например, персистентность, SSR и тп?

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

      По поводу debounce в RRC:

      • Для query используется throttling - пока идет запрос с определенными параметрами, другие с теми же параметрами отменяются.

      • Для мутаций используется debounce - каждая следующая мутация отменяет предыдущую, если та еще не завершилась. Для этого вторым параметром в мутации передается abortController.signal.

      Race condition там нет.


      1. clerik_r
        14.09.2024 12:30

        • Здесь использована магия (не все любят магию) mobx с ООП (многие считают эту парадигму неудачной), обертками компонент (увеличение vdom, стектрейса). Вынесем это за скобки ибо холивар разводить не хотелось бы.

        Object getter/setter магия?) Ну ок) Магия так магия)))
        https://stackblitz.com/edit/vitejs-vite-fkgny3?file=src%2Fmain.ts&terminal=dev

        • Использована какая то неизвестная мне функция asyncHelpers для debounce и отмены запросов.

        Ну да, она легко имплементируется самостоятельно, ее АПИ же видно и понятно как она устроена и работает

        • Использован неизвестный мне класс ApiReq (опять ООП) в тч для кэширования запросов.

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

        Причем в данной реализации нет возможности принудительно запустить обновление данных, например через Pull to refresh - данные минуту всегда будут приходить из кэша. Неприятный баг UX.

        Так то вообще легко, например можно в fetchData добавить аргумент force который в withCahe передаст null, что будет означать что кэш мы игнорируем и делаем запрос, опять же т.к. реализация своя можно всё что угодно добавлять.

        • fetching каждый раз переходит в true, а в конце в false, тем самым показывая на короткое время спинер даже если данные уже закэшированы - UI баг. Возможно потребуется серьезный рефакторинг чтобы это исправить, если не полный отказ от данной архитектуры. Тут нужно смотреть на реализацию ApiReq.

        Т.к. MobX сконфигурирован на асинхронные реакции(включая автобатчинг), а они запускаются путём setTimeout, а внутри функции у нас промисты(микротаски), в момент испускания реакции значение fetching будет неизменным true и никаких спиннеров на короткое время не будет.
        Вот как выглядит данная конфигурация

        Вот как это в действии https://stackblitz.com/edit/vitejs-vite-vslbhn?file=src%2Fmain.ts&terminal=dev

        • Отсутствует нормализация - плюс множество проблем консистенстности данных в приложении, и увеличивается количеств запросов к серверу. Возможно придется полностью переписать архитектуру, чтобы ее прикрутить.

        Вы о чем вообще? Пример просто из потолка написан, тут полная свобода, нужна нормализация - легко, не нужна - легко. Причем тут кол-во запросов к серверу? С чего вдруг что-то переписывать надо? Обрабатывайте данные как душе угодно.

        • Данные item можно показать еще пока не погрузились комментарии, но не в данном примере. Потребуется рефакторинг.

        Легко, просто код чутка измените под эти нужды и всё, элементарно же.

        • Легко ли прикрутить, например, персистентность?

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

        Именно поэтому лучше не городить "свои решения", а использовать библиотеку, где большинство моментов уже продумали.

        Т.е. по вашему все вокруг идиоты, включая меня. И решения написанные нами это дно и их лучше не использовать. А вот если использовать библиотеку которую кто-то там написал(в том числе вы), вот уже совсем другое дело, там настоящий уровень и все так прекрасно и удобно. А если мне/Васе/Пете и т.п. не нравится то, что есть? Как быть? Ну не смешите такими выводами странными. Такие "выводы" канают только для начинающих.

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

        Вы сами придумали "проблемы", которые кстати решаются по щелчку пальца, ибо описанное вами поведение нужно конкретно вам, а конкретно мне нужно другое поведение, а конкретно Васе нужно своё поведение и т.п.

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

        Да любой проект, который писали опытные люди, в том числе классическое решение это функция/класс обертка для запросов к АПИ, в том числе события, на которые можно подписаться и перехватывать запросы/модицифировать их/универсальную обработку ошибок делать/ и т.д. и т.п. Так же классика это когда компоненты, которые используются в разных местах лежат в src/components и т.п.


        1. gen1lee Автор
          14.09.2024 12:30

          Из того что вы ответили получается что вы практически реализовали свой фреймворк на базе mobx, который используете от проекта к проекту, но только не оформили его в библиотеку. Что то в этом фреймворке требует изучения далеко не очевидной конфигурации mobx (как пример где fetching всегда false), что то не реализовано совсем - нормализация, пагинация, персистентность и мн. др., тот же optimistic response, и разумеется чтобы их добавить "просто код чутка измените под эти нужды и всё, элементарно же". Вот только многое из этого не элементарно и потребует больших изменений, довольно багоемких, которые в идеале стоило бы еще и тестами покрыть.

          Оформите вашу идею в библиотеку, добавьте туда многое из того что обсудили и то что есть у "конкурентов", и возможно любители mobx ее оценят.


          1. clerik_r
            14.09.2024 12:30

            Из того что вы ответили получается что вы практически реализовали свой фреймворк на базе mobx, который используете от проекта к проекту, но только не оформили его в библиотеку.

            Ну по сути да, за 12 лет из которых 8 лет чистого фронта и множество разных проектов, грех не обзавестись кучей решений и подходов на все случаи жизни

            Что то в этом фреймворке требует изучения далеко не очевидной конфигурации mobx (как пример где fetching всегда false)

            Не надо ничего изучать, тебе сказали 1 раз, реакции асинхронные по умолчанию, отсюда и автобатчинг, и всё, ты это услышал и понял, 5 секунд изучения)

            что то не реализовано совсем - нормализация, пагинация, персистентность и мн. др., тот же optimistic response, и разумеется чтобы их добавить "просто код чутка измените под эти нужды и всё, элементарно же"

            Ну вообще да, без шуток. Любой каприз, берешь и делаешь.

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

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

            Оформите вашу идею в библиотеку, добавьте туда многое из того что обсудили и то что есть у "конкурентов", и возможно любители mobx ее оценят.

            И лишать разработчиков самой важной вещи - думать своей головой, чтобы для них все более и более элементарные вещи становились "проблемой" и под каждый чих они искали библиотеку. Ну нет, не такое будущее разработки я хочу видеть. Эти вещи слишком тривиальные чтобы пользоваться ими в рамках библиотек.


            1. gen1lee Автор
              14.09.2024 12:30
              +1

              У меня другой опыт - я насмотрелся на опытных разработчиков, реализовывающих "каждый чих" самостоятельно, и все это многократно переписывал. В 95% случаев (если не чаще) все это работает плохо, забаговано, и на больших проектах не подлежит исправлению кроме как "выкинуть и написать с нуля". Про очень многие из лучших паттернов они даже и не в курсе (нормализация, optimistic response и мн. др.). Эти разработчики кстати чаще всего сами не видят никаких проблем в своем коде, и уж тем более не видят проблем в UX приложения.


              1. clerik_r
                14.09.2024 12:30

                У меня другой опыт - я насмотрелся на опытных разработчиков, реализовывающих "каждый чих" самостоятельно, и все это многократно переписывал.

                Для начала с чего вы взяли что они реально опытные?) Например если человек Bob 15 лет работал на 2-3 проектах(а я таких знаю реально, прям лично), и человек Tom работал 5 лет на 10+ проектах, то Tom в 95% случаем будет на 3-4 головы лучшим разработчиком чем Bob который в опыте в годах аж на 10 лет больше работал. И это Tom построит гораздо более лучшую архитектуру на новом проекте, чем Bob, как минимум тупо за счет того что Tom видел в 3-4 раза больше проектов чем Bob.

                Ну и конкретно в вашем сценарии вы видели плохих разработчиков, к великому сожалению их большинство( Так же как установщиков кондиционеров в лучшем случае 9 из 10 - плохие откровенно говоря, как тех кто ремонтирует автомобили, и т.д и т.п. Просто такова природа человека, в этом нет ничего удивительного и необычного, более того, таких людей никакие библиотеки не спасут, вот прям от слова совсем.

                Откройте ваш проекты/любой проект допустим реактовский и посмотрите сколько в нем в зависимостях библиотек, а теперь внимание, по вашей логике код на этим проектах должен быть в худшем случае идеальный. а если там библиотек больше 20-30, так вообще великолепный, без вариантов, ведь люди столько идеальных готовых решений примели и у них просто нет шансов на ошибки и на плохой код.

                А теперь окунемся в реальность. в лучшем случае код на этих проектах будет очень средненьким с натяжкой, а в среднем плохим, иногда ужасным.
                Какая мораль? Никакие библиотеки/фреймворки и т.п. ни от чего не спасают(разве что чуть чуть), если руки из нужного места растут, то все будет отлично, как без библиотек, так и с библиотеками, а если нет, то соответственно все будет плохо.

                В 95% случаев (если не чаще) все это работает плохо, забаговано, и на больших проектах не подлежит исправлению кроме как "выкинуть и написать с нуля".

                Возможно вы сами себе придумали критерии и по ним оценили что всё работает плохо. А если посмотреть объективно со стороны, то по факту все работает как минимум нормально. а то и хорошо.

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

                Зависит от ваших индивидуальных критериев к коду. Мало ли, может вы пишете вложенные друг в друга теранарки, или не делаете early return в функциях(в том числе реакт компонентах), или вместо async/await пишете цепочки .then и т.д. и т.п. И считаете что это нормально и правильно, а другой код плохой.
                Хотя если быть честными самими с собой, то код, который ты читаешь первый раз сверху вниз и понимаешь что он делает, что будет дальше, какой будет результат его выполнения - это хороший код.
                А если ты смотришь и не понимаешь, без документации, без задавания вопросов автору кода и т.п. - этот код плохой.
                Но бывают и исключения из этих правил. в случаях когда очень сложная и навороченная бизнес логика, в таких случаях код будет плохим неизбежно и без вариантов. Где-то чуть менее плохим, где-то чуть более плохим, но суммарно плохим.


  1. rickets
    14.09.2024 12:30

    redux....


    1. gen1lee Автор
      14.09.2024 12:30

      На хабре принято писать конструктивно - это же не религиозный культ очередного убийцы redux.


      1. markelov69
        14.09.2024 12:30

        Он имеет в виду, что использовать redux(и всё что вокруг и около него) вместо mobx в наше время, это нелепо. Да и в 2019 году уже было нелепо)


        1. gen1lee Автор
          14.09.2024 12:30

          Я думаю что судя по его комментам он бы сказал то же и про mobx в угоду effector, а я бы сказал так про mobx и effector в угоду redux) А в соседней статье любители хайпа отказываются от effector после года мучений и в очередной раз выбирают технологию без должного анализа)

          Вот только профессионалы как использовали redux и не имели с ним никаких проблем, так и используют. Потому что это самый простой, а значит лучший инструмент.