Гайд по освоению комплексных модальных потоков React Native.
Привет Хабр! Представляю вам перевод статьи What Everyone Is Getting Wrong About React Native Modals. Так же я веду канал по фронтенду на котором часто публикую полезные для разработчиков материалы и новости. А теперь к статье!
Использование модальных окон (попапов) в React Native кажется вам занозой в заднице? Вы в этом не одиноки. Постоянно контролировать открытое состояние и повторять код везде, где хотите их использовать — это довольно утомительно.
Проблема только усложняется, когда вы пытаетесь создать сложные попапы. Сделали две модалки и все, главный компонент превращается в кашу, и состояние становится беспорядочным. Я испытал это на своем опыте во многих больших компаниях вроде Zé Delivery (от AB-InBev), Alfred Delivery и сейчас в X-Team.
Не отчаивайтесь! Многие думают, решение есть. И все же, в этой статье я объясню, с чего начинаются проблемы и как их элегантно решить, прокачав при этом опыт разработки.
Это даже поможет вам использовать попапы внутри и снаружи компонентов React, например, в Saga.
Знания из этой статьи надежно заключены в этой библиотеке.
Трудности «простого потока»
Представьте: вы работаете на Facebook, и продуктовая команда просит вас использовать simple flow:
На месте компании, я хотел бы добавить модальное окно, предлагающее оценить приложение от 0 до 5 звезд после того, как пользователю впервые понравится пост. Есть два варианта развития событий:
a. Юзер оценивает меньше, чем на 4 звезды — показывать другое окно, спрашивающее о фидбеке для улучшения качества сервиса.
b. В другом случае, показывать «бодрый» попап с просьбой оценить нас в App Store.
В конце надо показать модалку вроде «Спасибо за поддержку».
Не так уж заумно, правда же?
Спустя четыре модалки, со стороны разработчика возникает множество вопросов. Где эту логику нужно вставить? Как мы должны обрабатывать вывод из последней? Как сделать все чисто и понятно?
Можете ли вы определить, каким будет код? Можете представить, как вы пишете сложную логику для обработки порядка попапов, проверяете, перешла ли каждая модалка в невидимый режим. Расставляете кучу хуков useStates по местам?
Рост масштабов проблемы
Допустим, вы наконец с этим закончили и продуктовой команде нравится результат. Теперь они хотят поменять этот флоу, чтобы он появлялся только один раз, когда пользователь впервые что-то лайкнет — пост, коммент или продукт. Как бы вы подошли к этой задаче?
Вы могли бы скопипастить 4 попапа на каждом экране, где можно что-либо лайкнуть. Может, вы бы пошли дальше, и сделали из всего этого один компонент. Даже в этом случае компонент обертки на каждый экран все равно надо обязательно добавлять.
Проходит несколько месяцев, команда разработки увидела, как тяжело обрабатывать лайки на каждом экране, и хотят отдать эту обязанность Redux Saga, где функцию можно вызвать из любого места.
Как показывать модалку только при запуске действия из Saga? Вот ответ. Действия из Саги запускаются вне компонентов React.
Могу с уверенностью сказать, что я испытал все эти сценарии. Работая в сфере доставки еды, мы постоянно просили юзеров оценить приложение, доставку и покупки.
Как же этого избежать?
Сначала давайте разберемся с состоянием. Управление им — одна из самых важных вещей при работе с модальными окнами.
Раскройте внутренние свойства с помощью useImperativeHandle
Если вкратце, хук useImperativeHandle
позволяет раскрывать внутренние свойства через Ref
(реф). Если у вас есть модальный компонент, вы можете использовать useImperativeHandle
для раскрытия его функций show
и hide
. Это означает, что он может заниматься своим собственным состоянием, не делегируя его родительскому компоненту с пропсами. Это может быть полезно для чистоты кода и для того, чтобы избежать передачи множества пропсов. Давайте посмотрим на примере:
Фрагмент кода
import React, { useState, useImperativeHandle } from 'react';
import { Text } from 'react-native';
import ModalContainer from 'react-native-modal';
export const ExampleModal = React.forwardRef((ref) => {
const [isVisible, setIsVisible] = useState(false);
const show = () => setIsVisible(true);
const hide = () => setIsVisible(false);
useImperativeHandle(ref, () => ({ hide, show }));
return (
<ModalContainer onBackdropPress={hide} isVisible={isVisible}>
<Text>My awesome modal!</Text>
</ModalContainer>
);
});
Хоть это и шаг в правильном направлении, он не решает всех проблем. А именно:
Чтобы использовать этот метод, нам нужно передать свойство
Ref
, а значит, мы не можем применитьshow
вне компонентов React.Нам все еще нужно создавать инстанс компонента на каждом экране. Использовать на нескольких сразу без повтора
ExampleModal
невозможно.Внешне раскрывает
ref
компонента.
Мало кто знает, но у реакта есть метод createRef
, который можно использовать вне компонентов React. По факту, это есть в документации React-Navigation для пограничных состояний (edge кейсов).
На практике гибкость, которую вносит этот способ, заметна здесь:
Фрагмент кода
import React, { useState, useImperativeHandle } from 'react';
import { Text } from 'react-native';
import ModalContainer from 'react-native-modal';
export const imperativeModalRef = React.createRef();
export const SmartExample = () => {
const [isVisible, setIsVisible] = useState(false);
const show = () => setIsVisible(true);
const hide = () => setIsVisible(false);
useImperativeHandle(imperativeModalRef, () => ({ hide, show }));
return (
<ModalContainer onBackdropPress={hide} isVisible={isVisible}>
<Text>My awesome modal!</Text>
</ModalContainer>
);
};
Сейчас imperativeModalRef
можно импортировать откуда угодно, если корневым узлом будет SmartExample
. Теперь мы можем использовать show
и hide
из каждого компонента, из функции или Саги.
Именно здесь вы можете поиграться с абстракциями, чтобы заставить SmartExample
отображать какую угодно модальную форму. Один из способов сделать это — заставить функцию show
получать компонент и отображать его.
Принимаем дополнительные меры
Вместо того, чтобы заставлять вас заново изобретать колесо, я создал открытую библиотеку в которой содержатся все эти концепции и многое другое, с полной поддержкой TypeScript на основе react-native-modal
.
Вот основная идея ее использования:
«Слова ничего не стоят. Покажите мне код» ― Линус Торвальдс.
Пример использования react-native-magic-modal
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { MagicModalPortal, magicModal } from 'react-native-magic-modal';
const ConfirmationModal = () => (
<View>
<TouchableOpacity onPress={() => magicModal.hide({ success: true })}>
<Text>Click here to confirm</Text>
</TouchableOpacity>
</View>
);
const ResponseModal = ({ text }) => (
<View>
<Text>{text}</Text>
<TouchableOpacity onPress={() => magicModal.hide()}>
<Text>Close</Text>
</TouchableOpacity>
</View>
);
const handleConfirmationFlow = async () => {
// Мы вызываем его с пропсами или без в зависимости от требований модала.
const result = await magicModal.show(ConfirmationModal);
if (result.success) {
return magicModal.show(() => <ResponseModal text="Success!" />);
}
return magicModal.show(() => <ResponseModal text="Failure :(" />);
};
export const MainScreen = () => {
return (
<View>
<TouchableOpacity onPress={handleConfirmationFlow}>
<Text>Start the modal flow!</Text>
</TouchableOpacity>
<MagicModalPortal />
</View>
);
};
Как видите, этот способ дает большую гибкость, невозможную ранее, просто абстрагируя концепции, которые мы рассмотрели. Теперь формы подтверждения можно призывать откуда угодно — изнутри или снаружи компонентов React. Кроме того, он автоматически решает общие проблемы, связанные с модалками.
А вы знали, что в React Native невозможно одновременно показывать два попапа? Даже если вы попытаетесь показать их один за другим, у вас не получится, потому что последний до сих пор проигрывается в состоянии ‘close’. К счастью, этот вопрос уже решен с нашей стороны, об этом читайте здесь.
Эта же самая логика была опробована в бою в больших компаниях, где я работал (Zé Delivery by AB-InBev, Alfred Delivery, and X-Team).
Документация React Native Magic Modal проста для понимания, и она уже даст вам фору. Больше информации здесь.
Спасибо за чтение!