Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
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 в некоторых статьях признаются, что часто могут отказаться от понимаемого кода во имя минимизации строк, например.