
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 в некоторых статьях признаются, что часто могут отказаться от понимаемого кода во имя минимизации строк, например.