Почему иногда React/Redux в текущем состоянии give me creeps

от автора

React существует достаточно давно, чтобы мажорные изменения в этой библиотеке, не ощущались температурой подогрева кресел разработчиков в холодные зимние вечера (не благодарите за лайфхак). Но Facebook сделали ход конем и в свое время выпустили не мажорную, а минорную версию и тем самым сняли с себя ответственность за нестабильность уже существующих миллионов репозиториев, как вы уже поняли я буду рассказывать про версию 16.8.0, а так как мы почти никогда не используем React без Redux в продакшн репозиторияx, то и про него скажу. 

И сперва давайте поговорим про React. Почему была упомянута нестабильность после внесения “дополнений” 16.8.0, проблема в том что она произошла в головах разработчиков — легким движением руки Facebook сказал нам, знаете, ООП это конечно же хорошо, но функциональный подход лучше. И тут особо ярые и продвинутые ринулись кидать уже существующий подход Statefull Components и Stateless Components и дописывать новыe functional Components с его хуками useState, useCallback, useEffect etc. и только лишь иногда useContext.

Штош, в самих этих 4х функциях я ничего плохого и не вижу, в общем-то:

  • Динамическое именование проперти стейта — Fine

  • не нужно выстраивать структуру стейта и запоминать ее для обновления — Excellent

  • Можно использовать хук для нескольких изолированных экземпляров стейта даже в одном и том же компоненте — Splendid

  • А главное  — это не нужно запоминать все примочки с Lifecycle — тут все сразу понятно — срабатывает сразу после рендера, а если добавишь clean-up возвращаемую функцию то она сработает сразу же перед удалением компонента из дерева, чего уже говорить про то что строк кода нужно писать меньше — Amazing (c) Тим Кук

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

import React, { useState, useEffect } from 'react';  function useFriendStatus(friendID) {   const [isOnline, setIsOnline] = useState(null);    function handleStatusChange(status) {     setIsOnline(status.isOnline);   }    useEffect(() => {     ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);     return () => {       ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);     };   });    return isOnline; }

А теперь давайте этот пример расширим до жизненных реалий (все хуки в разных файлах):

// useFriednStatus.js  import React, { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import * as actions from 'actions'; import { getLoggedInUserSelector } from 'selectors';  function useFriendStatus(friendID) {   const [isOnline, setIsOnline] = useState(null);    function handleStatusChange(status) {     setIsOnline(status.isOnline);   }    useEffect(() => {     ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);     return () => {       ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);     };   });    return isOnline; }  // useBestFriendNotifier.js  import React, { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import * as actions from 'actions'; import { getLoggedInUserSelector } from 'selectors';  function useBestFriendNotifier(currentUserId) {     const loggedInUser = useSelector(getLoggedInUserSelector);      const isBestFriendOnline = useFriendStatus(loggedInUser.bestFriendId);     const dispatch = useDispatch();      const notifyMeAboutBestFriendActivity = React.useCallback(() => {         dispatch(actions.notify(isBestFriendOnline));     }, [isBestFriendOnline]);      return notifyAboutBestFriendActivity; }  // Notification.jsx  import { bestFriendOnlineSelector } from 'selectors';  function Notification(({ id }) => {     const notifier = useBestFriendNotifier(id);     const isOnline = useSelector(bestFriendOnlineSelector);      return (         <p>Your Best friend is { isOnline ? 'Online' : 'Offline' }</p>     ); });

Эта вложенность может быть и больше, а код уже нечитаемый, приходиться заниматься рекурсивным чтением чтобы понять что было в самом начале и откуда берётся значение, чтобы, например, найти в каком поле Store храниться текущий пользователь.

Как всегда выходит в данном случае, все что ты даешь разработчику написать самому может стать предметом долгих обсуждений. И можно сказать дискоммуникация, плохой лид, плохой разраб, отсутствие конвенции, но если так подумать: есть новый непроторенный ни лидом, ни другими разработчиками подход, и как бы и в официальной документации НЕ написано жирными буквами:

Please, do not use more then one level nesting of custom hooks 

И вот у тебя уже в проекте Stateful Componetns поверх Stateless Componetns, а некоторые перекочевали в function Components (правильно, с хуками, которые под Stateful) и просто function Components, а все потому что нам заботливо написали в документации.

We don’t recommend rewriting your existing components overnight but you can start using Hooks in the new ones if you’d like.

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

Ну и пару слов хочется сказать про redux-thunk, та же проблема — вложенные dispatch() в dispatch(), несколько dispatch() вызовов в одном action где один dispatch() может вызывать чистый action with type … payload, а вот другие иметь больше вложенных dispatch() и даже с TypeScript не всегда удается отследить отправляемый пейлоад:

function App(() => {     dispatch(actions.init()); });  // actions.ts  export const init = (): ThunkAction<void, RootState, void, AppAction>  =>  {     return async (dispatch, getState) => {         try {             const user = await getUser();             if (!user) {                 dispatch(setNotLoggedInUserState());             }              dispatch(setLoggedInUserState(user));         } catch (e) {             dispatch(showErrorModal);         }     }; }  const setNotLoggedInUserState =  (): ThunkAction<void, RootState, void, AppAction> => {     return async (dispatch, getState) => {         dispatch(setDefaults());         dispatch(showLoginModal());     }; }  const setLoggedInUserState = (user): ThunkAction<void, RootState, void, AppAction> => {     return async (dispatch, getState) => {         dispatch(wellcomeBackModal(user.name));     }; }  // ...

Заключение

Это всего лишь рассуждения на тему того что не всегда модное === лучшее, и возможно, стоило бы уступить место более понимаемому подходу если все равно профита в перфоманс приложения не наблюдается. Впрочем, сами разработчики Facebook в некоторых статьях признаются, что часто могут отказаться от понимаемого кода во имя минимизации строк, например.

ссылка на оригинал статьи https://habr.com/ru/post/565904/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *