Как стать автором
Обновить

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

Время на прочтение5 мин
Количество просмотров5.4K

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

Теги:
Хабы:
0
Комментарии64

Публикации

Изменить настройки темы

Истории

Работа

Ближайшие события

PG Bootcamp 2024
Дата16 апреля
Время09:30 – 21:00
Место
МинскОнлайн
EvaConf 2024
Дата16 апреля
Время11:00 – 16:00
Место
МоскваОнлайн
Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн