Компоненты/Тост

Тост

31 января 2025

Полноценная система тостов с глобальным API и стопкой анимаций

Установка

npx astra-rn add toast
pnpm dlx astra-rn add toast
yarn dlx astra-rn add toast
bunx --bun astra-rn add toast

Файлы

index.tsx
НазадЛента дней с липкой датойДалееОбмен токенов

Вручную

Поставьте зависимости:

npm install expo-haptics react-native-gesture-handler react-native-reanimated react-native-safe-area-context react-native-svg react-native-worklets
pnpm add expo-haptics react-native-gesture-handler react-native-reanimated react-native-safe-area-context react-native-svg react-native-worklets
yarn add expo-haptics react-native-gesture-handler react-native-reanimated react-native-safe-area-context react-native-svg react-native-worklets
bun add expo-haptics react-native-gesture-handler react-native-reanimated react-native-safe-area-context react-native-svg react-native-worklets

Скопируйте эти файлы в src/shared/components/molecules/Toast/:

Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.

Использование

import { Fragment, useMemo, useRef } from "react";
import { Dimensions, StyleSheet, Button } from "react-native";
import { SymbolView } from "expo-symbols";
import { useSafeAreaInsets } from "react-native-safe-area-context";

import { toast, Toaster } from "@/shared/components/molecules/Toast";
import type { IToasterHandle, IToastSwipeAction, TToastPosition } from "@/shared/components/molecules/Toast";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";
import { View } from "react-native";
const _width = Dimensions.get("window").width;
const BACK_PILL_CLEARANCE = 28;
const SWIPE_ACTION: IToastSwipeAction = {
  label: "Delete",
  color: "#FF3B30",
  direction: "left" as const,
  icon: () => <SymbolView name="trash.fill" size={18} tintColor={"#fff"} />,
  commitOffset: _width * 0.8,
  onCommit: () => {},
};

const CUSTOMIZE = [
  { key: "position", label: "Позиция", type: "select", options: [{ value: "top", label: "Сверху" }, { value: "bottom", label: "Снизу" }], default: "top" },
  { key: "theme", label: "Тема", type: "select", options: [{ value: "dark", label: "Тёмная" }, { value: "light", label: "Светлая" }], default: "dark" },
  { key: "successTitle", label: "Заголовок", type: "text", default: "Success", maxLength: 32 },
  { key: "successText", label: "Описание", type: "text", default: "Your changes have been saved.", maxLength: 60 },
  { key: "offset", label: "Отступ", type: "slider", min: 0, max: 120, step: 1, unit: "px", default: 30 },
  { key: "gap", label: "Зазор", type: "slider", min: 0, max: 40, step: 1, unit: "px", default: 14 },
  { key: "visibleToasts", label: "Видимых", type: "slider", min: 1, max: 6, step: 1, default: 3 },
  { key: "duration", label: "Время показа", type: "slider", min: 1000, max: 15000, step: 500, unit: "ms", default: 4000 },
  { key: "closeButton", label: "Крестик", type: "toggle", default: false },
  { key: "swipeLabel", label: "Текст свайпа", type: "text", default: "Delete", maxLength: 16 },
  { key: "swipeColor", label: "Цвет свайпа", type: "color", default: "#FF3B30" },
  { key: "interval", label: "Пауза автопоказа", type: "slider", min: 150, max: 2000, step: 50, unit: "ms", default: 500 },
] as const;

export default function ToastScreen() {
  const custom = useCustomize(CUSTOMIZE);
  const position = custom.position as TToastPosition;
  const insets = useSafeAreaInsets();
  const toasterRef = useRef<IToasterHandle>(null);
  const customRef = useRef(custom);
  customRef.current = custom;
  const { swipeLabel, swipeColor } = custom;
  const swipeAction = useMemo<IToastSwipeAction>(
    () =>
      swipeLabel === SWIPE_ACTION.label && swipeColor === SWIPE_ACTION.color
        ? SWIPE_ACTION
        : { ...SWIPE_ACTION, label: swipeLabel, color: swipeColor },
    [swipeLabel, swipeColor],
  );

  const handleSuccess = () =>
    toast.success(customRef.current.successTitle, {
      description: customRef.current.successText,
    });
  const handleError = () =>
    toast.error("Connection lost", {
      description: "Something went wrong. Please try again.",
    });
  const handleInfo = () =>
    toast.info("New version available", {
      description: "Restart the app to update.",
    });
  const handleWarning = () =>
    toast.warning("Low storage", {
      description: "Less than 200 MB left on this device.",
    });
  useAutoplay(async ({ wait }) => {
    const ids: (string | number)[] = [];
    try {
      await wait(customRef.current.interval);
      ids.push(handleSuccess());
      await wait(customRef.current.interval);
      ids.push(handleError());
      await wait(customRef.current.interval);
      ids.push(handleInfo());
      await wait(customRef.current.interval);
      ids.push(handleWarning());
      await wait(customRef.current.interval);
      ids.push(handleSuccess());
      await wait(customRef.current.interval);
      toasterRef.current?.expand();
      await wait(900);
      for (const id of [ids[2], ids[3], ids[1], ids[4], ids[0]]) {
        toasterRef.current?.swipeAway(id);
        await wait(1300);
      }
      await wait(400);
    } finally {
      toasterRef.current?.collapse();
    }
  });

  return (
    <Fragment>
      <Showcase disableBackButton={true}>
        <View style={styles.stage}>
          <Button title="Success" onPress={handleSuccess} />
          <Button title="Error" onPress={handleError} />
          <Button title="Info" onPress={handleInfo} />
          <Button title="Warning" onPress={handleWarning} />
        </View>
        <Toaster
          ref={toasterRef}
          position={position}
          swipeAction={swipeAction}
          offset={custom.offset}
          style={
            position === "top"
              ? { top: insets.top + custom.offset + BACK_PILL_CLEARANCE }
              : undefined
          }
          gap={custom.gap}
          visibleToasts={custom.visibleToasts}
          duration={custom.duration}
          closeButton={custom.closeButton}
          theme={custom.theme as "dark" | "light"}
        />
      </Showcase>
    </Fragment>
  );
}

const styles = StyleSheet.create({
  stage: {
    justifyContent: "center",
    alignItems: "center",
    flex: 1,
  },
});

Раскрытие стопки из кода

Необязательный ref у Toaster с типом IToasterHandle даёт методы expand(), collapse() и toggle() — раскрывают и сворачивают стопку так же, как тап по ней. Без ref стопка раскрывается только касанием, как и раньше.

const toasterRef = useRef<IToasterHandle>(null);

<Toaster ref={toasterRef} />;

toasterRef.current?.expand();

Пропсы

Пропс
Описание
id

Тип TToastId

type?

Тип TToastType

title?

Тип TToastContent

description?

Тип TToastContent

icon?

Тип ReactNode

jsx?

Тип ReactNode

duration?

Тип number

dismissible?

Тип boolean

position?

Тип TToastPosition

action?

Тип IToastAction

cancel?

Тип IToastAction

onDismiss?

Тип TOnDismiss

onAutoClose?

Тип TOnDismiss

promise?

Тип TPromise

swipeAction?

Тип IToastSwipeAction | null

style?

Тип StyleProp<ViewStyle>

delete?

Тип boolean

IToaster

Пропс
Описание
position?

Тип TToastPosition

duration?

Тип number

gap?

Тип number

visibleToasts?

Тип number

expand?

Тип boolean

offset?

Тип number

closeButton?

Тип boolean

swipeToDismiss?

Тип boolean

swipeDirection?

Тип TToastSwipeDirection

swipeAction?

Тип IToastSwipeAction

haptics?

Тип boolean

theme?

Тип TToastTheme

colors?

Тип Partial<TToastColors>

icons?

Тип IToastIcons

style?

Тип StyleProp<ViewStyle>

toastStyle?

Тип StyleProp<ViewStyle>

titleStyle?

Тип StyleProp<TextStyle>

descriptionStyle?

Тип StyleProp<TextStyle>

ref?

Тип Ref<IToasterHandle>

IToastAction

Пропс
Описание
label

Тип ReactNode

onPress

Тип (id: TToastId) => void