Redux 키트, Tailwind, Framer Motion을 사용하여 React Typescript에 알림/toast 시스템을 만드는 방법

이 짧은 글에서 알림/Toast 구성 요소를 구축할 것입니다.
본고의 목적은 단지 이러한 구성 요소를 어떻게 구축하는지에 영감을 제공하는 것이다.본고에서 어떤 내용도 자신의 의견을 고집하는 것이 없기 때문에 원한다면 다른 상태 관리자, 다른 파일 구조, 다른 스타일 시스템을 사용하십시오.
이런 유형의 구성 요소는 여러 가지 이름이 있는데, 서로 다른 이름은 서로 다른 사람에게 서로 다른 신호를 보낼 수 있지만, 본고는 사용자가 지정한 작업에 대한 응답을 알려주는 기본 구성 요소일 뿐이다. 예를 들어 설정 파일 정보 업데이트 등이다.
아래에서 완성된 프레젠테이션과 코드를 찾을 수 있습니다.
데모: Here
Github 저장소: Here
우리는 알림 구성 요소의 네 가지 변형인 성공, 경고, 오류, 정보를 구축할 것이다.
이 글은 코드를 빠르게 훑어볼 것이기 때문에 React 기반의 현대 개발 설정과 사용 도구에 대한 기본적인 이해가 필요하다. 왜냐하면 나는 서로 다른 부분을 깊이 있게 묘사하지 않기 때문이다.
사용한 도구:
Next.js
Redux Toolkit
Framer Motion
Tailwind
Radix UI
Radix colors
react-use
clsx
lodash
ms
npx create-next-app@latest --typescript name-of-project

기본 설정 및 Redux 키트
다음을 시작합니다.typescript의 js 프로젝트를 사용하면 Redux를 설정하는 것부터 시작할 것입니다. 이를 위해, 우리는 공식적이고 독선적이며 배터리를 포함하는 도구 모음을 사용하여 효율적인 Redux 개발을 진행할 것입니다. Redux Toolkit
여기서부터 src 폴더를 만들고 src 내부에 app 폴더를 만들고 features 폴더를 만들고 redux 폴더를 만듭니다.다음 단계에서도 기본값을 이동합니다.안에 jspages 폴더src가 있습니다.
이것은 우리의 기본 프로젝트 구조가 될 것이다.
프로젝트를 어떻게 구성하든지, 폴더를 어떻게 명명하든지, 이것은 내가 즐겨 사용하는 기본적인 기준일 뿐이다.
각 특성은 features 폴더에 넣고 자신의 구성 요소, 연결, Redux 상태 슬라이드가 있습니다.우리는 Notifications를 응용 프로그램의'기능'으로 간주할 것이다.
redux 폴더에는 hooks.ts, rootReducer.ts, store.tsstore.ts 세 개의 파일이 생성됩니다.
세 파일에는 기본 Redux 설정이 포함됩니다.@redux/rootreducer 파일에는 글로벌 Redux 스토어의 기본 설정이 포함됩니다.이것은 우리의 서로 다른 감속기를 포함하고, 다른 종류의 조수를 내보낼 것입니다. 이 조수들은 전체 프로젝트에서 사용할 것입니다.
// src/redux/store.ts
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'
import { rootReducer } from '@redux/rootReducer'

export const store = configureStore({
  reducer: rootReducer,
})

export type AppDispatch = typeof store.dispatch
export type RootState = ReturnType<typeof store.getState>
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>
가져오기tsconfig.json를 참조하십시오.tsconfig 경로가 이 작업에 이미 사용되었습니다.환매 협의 중의 tsconfig.paths.jsonrootReducer.ts 문서를 참고하십시오.
현재 notificationsReducer에서 Redux 루트 감속기를 설정할 것입니다. 이것은 전체 프로젝트에서 만들 수 있는 모든 다른 감속기를 포함합니다.
// src/redux/rootReducer.ts
import { combineReducers } from '@reduxjs/toolkit'

import notificationsReducer from '@features/notifications/notification.slice'

export const rootReducer = combineReducers({
  notifications: notificationsReducer,
})
rootReducer가 생성되지 않은 hooks.ts를 가져오고 있습니다.우리는 곧 이것을 만들 것이다.
마지막으로 features에서 전체 프로젝트에서 사용할 수 있도록 일반적인 Redux 갈고리를 내보냅니다.
// src/redux/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from '@redux/store'

export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
이 갈고리들은 기본적으로 일반적인 Redux 갈고리에 유형 안전성을 증가시켰을 뿐이다.
이 모든 기본 설정은 Redux Toolkit documentation에서 찾을 수 있습니다.

알림 Redux 슬라이스 만들기notifications에서 우리는 notifications.slice.ts 기능을 만들고 이 기능에 Notification 파일을 만들 것이다. 이 파일에는 우리의 축배/통지의 모든 Redux 논리가 포함될 것이다.
우리는 먼저 알림 상태의 외관과 상태 슬라이드 자체를 정의할 것이다.
// src/features/notifications/notifications.slice.ts
type NotificationsState = {
  notifications: Notification[]
}

const initialState: NotificationsState = {
  notifications: [],
}

const notificationsSlice = createSlice({
  name: 'notifications',
  initialState,
  reducers: {},
})
상태 필름에서 사용하는 src/features/notifications 형식은 잠시 후 알림 구성 요소 자체에 정의됩니다.보아하니 이렇다.
// src/features/notifications/NotificationItem.tsx
export type NotificationTypes = 'success' | 'error' | 'warning' | 'info'

export type Notification = {
  /**
   * The notification id.
   */
  id: string

  /**
   * The message of the notification
   */
  message: string

  /**
   * An optional dismiss duration time
   *
   * @default 6000
   */
  autoHideDuration?: number

  /**
   * The type of notification to show.
   */
  type?: NotificationTypes

  /**
   * Optional callback function to run side effects after the notification has closed.
   */
  onClose?: () => void

  /**
   * Optionally add an action to the notification through a ReactNode
   */
  action?: ReactNode
}

그리고 추가/취소 알림을 처리하기 위해 다른 축소기를 추가할 것입니다.
// src/features/notifications/notifications.slice.ts
const notificationsSlice = createSlice({
  name: 'notifications',
  initialState,
  reducers: {
    /**
     * Add a notification to the list
     *
     * @param state - Our current Redux state
     * @param payload - A notification item without an id, as we'll generate this.
     */
    addNotification: (
      state,
      { payload }: PayloadAction<Omit<Notification, 'id'>>
    ) => {
      const notification: Notification = {
        id: nanoid(),
        ...payload,
      }

      state.notifications.push(notification)
    },
    /**
     * Remove a notification from the list
     *
     * @param state - Our current Redux state
     * @param payload - The id of the Notification to dismiss
     */
    dismissNotification: (
      state,
      { payload }: PayloadAction<Notification['id']>
    ) => {
      const index = state.notifications.findIndex(
        (notification) => notification.id === payload
      )

      if (index !== -1) {
        state.notifications.splice(index, 1)
      }
    },
  },
})
React 구성 요소에서 쉽게 사용할 수 있도록 적당한 위치에서 Reducer 논리를 사용할 것입니다. 선택기 함수를 만들고 내보내서 알림 상태를 선택하고 갈고리로 알림 상태 슬라이드를 완성할 것입니다.
우리는 또한 감속기 자체와 상응하는 재배열 조작을 출력할 것이다.
전체 파일은 다음과 같습니다.
// src/features/notifications/notifications.slice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { nanoid } from 'nanoid'

import type { Notification } from '@features/notifications/components/NotificationItem'
import type { RootState } from '@redux/store'
import { useAppSelector } from '@redux/hooks'

type NotificationsState = {
  notifications: Notification[]
}

const initialState: NotificationsState = {
  notifications: [],
}

const notificationsSlice = createSlice({
  name: 'notifications',
  initialState,
  reducers: {
    /**
     * Add a notification to the list
     *
     * @param state - Our current Redux state
     * @param payload - A notification item without an id, as we'll generate this.
     */
    addNotification: (
      state,
      { payload }: PayloadAction<Omit<Notification, 'id'>>
    ) => {
      const notification: Notification = {
        id: nanoid(),
        ...payload,
      }

      state.notifications.push(notification)
    },
    /**
     * Remove a notification from the list
     *
     * @param state - Our current Redux state
     * @param payload - The id of the Notification to dismiss
     */
    dismissNotification: (
      state,
      { payload }: PayloadAction<Notification['id']>
    ) => {
      const index = state.notifications.findIndex(
        (notification) => notification.id === payload
      )

      if (index !== -1) {
        state.notifications.splice(index, 1)
      }
    },
  },
})

const { reducer, actions } = notificationsSlice

// Actions
export const { addNotification, dismissNotification } = actions

// Selectors
const selectNotifications = (state: RootState) =>
  state.notifications.notifications

// Hooks
export const useNotifications = () => useAppSelector(selectNotifications)

export default reducer


공지 구성 요소 생성하기components 아래에 Notifications.tsx 폴더를 만듭니다.알림 기능과 관련된 모든 구성 요소를 여기에 놓을 것입니다.
세 개의 구성 요소를 만들 것입니다.NotificationList.tsx, NotificationItem.tsx와 마지막Notifications.tsx.
Notification components
Google의 부모 구성 요소NotificationList는 알림 상태 슬라이스를 구독하고 출력NotificationItems 구성 요소를 Redux 슬라이스의 알림 목록에 비추어 여러 개childrenNotificationListNotificationList.tsx로 표시합니다.

상위 어셈블리에 공지
// src/features/ntoifications/components/Notifications.tsx
import { useNotifications } from '@features/notifications/notification.slice'

import { NotificationItem } from '@features/notifications/components/NotificationItem'
import { NotificationList } from '@features/notifications/components/NotificationList'

export const Notifications = () => {
  const notifications = useNotifications()

  return (
    <NotificationList>
      {notifications.map((notification) => (
        <NotificationItem key={notification.id} notification={notification} />
      ))}
    </NotificationList>
  )
}


알림 목록 구성 요소
우리의 NotificationItems 구성 요소는 우리의 모든 document.body 구성 요소를 수용할 것이다.React 포털 개념을 사용하여 DOM의 다양한 부분에 HTML을 렌더링합니다.제가 쓰는 것은 Portal component from Radix UI입니다.
기본적으로 포털은 NotificationList에 추가되지만 서로 다른 용기를 사용할 수 있습니다.
OutNotifications에서는 프레임 동작 애니메이션 어셈블리를 사용하여 단일 알림 항목을 패키지화하여 위치 변경과 같은 애니메이션을 쉽게 설정할 수 있습니다.
import * as Portal from '@radix-ui/react-portal'
import type { ReactNode } from 'react'
import { AnimatePresence, AnimateSharedLayout } from 'framer-motion'

type Props = {
  children: ReactNode
}

export const NotificationList = ({ children }: Props) => {
  return (
    <Portal.Root>
      <AnimateSharedLayout>
        <ul
          aria-live="assertive"
          className="flex fixed z-50 flex-col gap-4 m-4 lg:m-8 pointer-events-none"
        >
          <AnimatePresence initial={false}>{children}</AnimatePresence>
        </ul>
      </AnimateSharedLayout>
    </Portal.Root>
  )
}


알림 항목 구성 요소
알림 항목 자체는 알림 텍스트를 보여주는 구성 요소로 아이콘과 유형에 따라 알림을 닫는 방법과 알림을 닫을 때 실행할 수 있는 선택할 수 있는 리셋을 제공합니다.
알림 등에서 사용자 정의 조작을 실현할 수도 있지만, 이 프레젠테이션에서는 간단합니다.

알림 항목 유형
// src/features/notifications/components/NotificationItem.tsx
export type NotificationTypes = 'success' | 'error' | 'warning' | 'info'

export type Notification = {
  /**
   * The notification id.
   */
  id: string

  /**
   * The message of the notification
   */
  message: string

  /**
   * An optional dismiss duration time
   *
   * @default 6000
   */
  autoHideDuration?: number

  /**
   * The type of notification to show.
   */
  type?: NotificationTypes

  /**
   * Optional callback function to run side effects after the notification has closed.
   */
  onClose?: () => void

  /**
   * Optionally add an action to the notification through a ReactNode
   */
  action?: ReactNode
}

type Props = {
  notification: Notification
}

항목 동작 방향 및 위치 공지
이것은 단지 프레젠테이션을 위해 서로 다른 렌더링 위치 사이를 쉽게 전환하는 데 필요한 것이다.현실 세계의 응용 프로그램에서 모든 알림을 표시할 위치를 선택할 수도 있다.
// src/features/notifications/components/NotificationItem.tsx
/**
 * To handle different positions of the notification, we need to change the
 * animation direction based on whether it is rendered in the top/bottom or left/right.
 *
 * @param position - The position of the Notification
 * @param fromEdge - The length of the position from the edge in pixels
 */
const getMotionDirectionAndPosition = (
  position: NotificationPositions,
  fromEdge = 24
) => {
  const directionPositions: NotificationPositions[] = ['top', 'bottom']
  const factorPositions: NotificationPositions[] = ['top-right', 'bottom-right']

  const direction = directionPositions.includes(position) ? 'y' : 'x'
  let factor = factorPositions.includes(position) ? 1 : -1

  if (position === 'bottom') factor = 1

  return {
    [direction]: factor * fromEdge,
  }
}


알림 항목 동작 변형(프레임 동작)
이것은 Framer Motion variants 알림 항목이 화면과 화면 아래에 있는 애니메이션 방식을 제어합니다.
// src/features/notifications/components/NotificationItem.tsx
const motionVariants: Variants = {
  initial: (position: NotificationPositions) => {
    return {
      opacity: 0,
      ...getMotionDirectionAndPosition(position),
    }
  },
  animate: {
    opacity: 1,
    y: 0,
    x: 0,
    scale: 1,
    transition: {
      duration: 0.4,
      ease: [0.4, 0, 0.2, 1],
    },
  },
  exit: (position) => {
    return {
      opacity: 0,
      ...getMotionDirectionAndPosition(position, 30),
      transition: {
        duration: 0.2,
        ease: [0.4, 0, 1, 1],
      },
    }
  },
}


알림 항목 구성 요소 구현
마지막으로 통지항의 실현 자체다.
export const NotificationItem = ({
  notification: { id, autoHideDuration, message, onClose, type = 'info' },
}: Props) => {
  const dispatch = useAppDispatch()
  const duration = useNotificationDuration() // Demo purposes
  const isPresent = useIsPresent()
  const position = useNotificationPosition() // Demo purposes
  const prefersReducedMotion = usePrefersReducedMotion()

  // Handle dismiss of a single notification
  const handleDismiss = () => {
    if (isPresent) {
      dispatch(dismissNotification(id))
    }
  }

  // Call the dismiss function after a certain timeout
  const [, cancel, reset] = useTimeoutFn(
    handleDismiss,
    autoHideDuration ?? duration
  )

  // Reset or cancel dismiss timeout based on mouse interactions
  const onMouseEnter = () => cancel()
  const onMouseLeave = () => reset()

  // Call `onDismissComplete` when notification unmounts if present
  useUpdateEffect(() => {
    if (!isPresent) {
      onClose?.()
    }
  }, [isPresent])

  return (
    <motion.li
      className={clsx(
        'flex w-max items-center shadow px-4 py-3 rounded border transition-colors duration-100 min-w-[260px] text-sm pointer-events-auto',
        notificationStyleVariants[type]
      )}
      initial="initial"
      animate="animate"
      exit="exit"
      layout="position"
      custom={position}
      variants={!prefersReducedMotion ? motionVariants : {}}
      onMouseEnter={onMouseEnter}
      onMouseLeave={onMouseLeave}
    >
      <div className="flex gap-2 items-center">
        {notificationIcons[type]}
        <span className="max-w-sm font-medium">{message}</span>
      </div>

      <div className="pl-4 ml-auto">
        <button
          onClick={handleDismiss}
          className={clsx(
            'p-1 rounded transition-colors duration-100',
            closeButtonStyleVariants[type]
          )}
        >
          <Cross2Icon />
        </button>
      </div>
    </motion.li>
  )
}

구성 요소의 다른 부분은 형식 기반 대상에서tailwind 클래스를 가져와서 스타일을 설정합니다.

알림 항목 구성 요소 전체 파일
import clsx from 'clsx'
import { ReactNode } from 'react'
import { motion, useIsPresent, type Variants } from 'framer-motion'
import { useTimeoutFn, useUpdateEffect } from 'react-use'

import {
  CheckCircledIcon,
  Cross2Icon,
  ExclamationTriangleIcon,
  InfoCircledIcon,
} from '@radix-ui/react-icons'

import {
  dismissNotification,
  NotificationPositions,
  useNotificationDuration,
  useNotificationPosition,
} from '@features/notifications/notification.slice'
import { useAppDispatch } from '@redux/hooks'
import { usePrefersReducedMotion } from '@app/core/hooks/usePrefersReducedMotion'

export type NotificationTypes = 'success' | 'error' | 'warning' | 'info'

export type Notification = {
  /**
   * The notification id.
   */
  id: string

  /**
   * The message of the notification
   */
  message: string

  /**
   * An optional dismiss duration time
   *
   * @default 6000
   */
  autoHideDuration?: number

  /**
   * The type of notification to show.
   */
  type?: NotificationTypes

  /**
   * Optional callback function to run side effects after the notification has closed.
   */
  onClose?: () => void

  /**
   * Optionally add an action to the notification through a ReactNode
   */
  action?: ReactNode
}

type Props = {
  notification: Notification
}

/**
 * To handle different positions of the notification, we need to change the
 * animation direction based on whether it is rendered in the top/bottom or left/right.
 *
 * @param position - The position of the Notification
 * @param fromEdge - The length of the position from the edge in pixels
 */
const getMotionDirectionAndPosition = (
  position: NotificationPositions,
  fromEdge = 24
) => {
  const directionPositions: NotificationPositions[] = ['top', 'bottom']
  const factorPositions: NotificationPositions[] = ['top-right', 'bottom-right']

  const direction = directionPositions.includes(position) ? 'y' : 'x'
  let factor = factorPositions.includes(position) ? 1 : -1

  if (position === 'bottom') factor = 1

  return {
    [direction]: factor * fromEdge,
  }
}

const motionVariants: Variants = {
  initial: (position: NotificationPositions) => {
    return {
      opacity: 0,
      ...getMotionDirectionAndPosition(position),
    }
  },
  animate: {
    opacity: 1,
    y: 0,
    x: 0,
    scale: 1,
    transition: {
      duration: 0.4,
      ease: [0.4, 0, 0.2, 1],
    },
  },
  exit: (position) => {
    return {
      opacity: 0,
      ...getMotionDirectionAndPosition(position, 30),
      transition: {
        duration: 0.2,
        ease: [0.4, 0, 1, 1],
      },
    }
  },
}

const notificationStyleVariants: Record<
  NonNullable<Notification['type']>,
  string
> = {
  success: 'bg-green-3 border-green-6',
  error: 'bg-red-3 border-red-6',
  info: 'bg-purple-3 border-purple-6',
  warning: 'bg-yellow-3 border-yellow-6',
}

const notificationIcons: Record<
  NonNullable<Notification['type']>,
  ReactNode
> = {
  success: <CheckCircledIcon />,
  error: <ExclamationTriangleIcon />,
  info: <InfoCircledIcon />,
  warning: <ExclamationTriangleIcon />,
}

const closeButtonStyleVariants: Record<
  NonNullable<Notification['type']>,
  string
> = {
  success: 'hover:bg-green-5 active:bg-green-6',
  error: 'hover:bg-red-5 active:bg-red-6',
  info: 'hover:bg-purple-5 active:bg-purple-6',
  warning: 'hover:bg-yellow-5 active:bg-yellow-6',
}

export const NotificationItem = ({
  notification: { id, autoHideDuration, message, onClose, type = 'info' },
}: Props) => {
  const dispatch = useAppDispatch()
  const duration = useNotificationDuration()
  const isPresent = useIsPresent()
  const position = useNotificationPosition()
  const prefersReducedMotion = usePrefersReducedMotion()

  // Handle dismiss of a single notification
  const handleDismiss = () => {
    if (isPresent) {
      dispatch(dismissNotification(id))
    }
  }

  // Call the dismiss function after a certain timeout
  const [, cancel, reset] = useTimeoutFn(
    handleDismiss,
    autoHideDuration ?? duration
  )

  // Reset or cancel dismiss timeout based on mouse interactions
  const onMouseEnter = () => cancel()
  const onMouseLeave = () => reset()

  // Call `onDismissComplete` when notification unmounts if present
  useUpdateEffect(() => {
    if (!isPresent) {
      onClose?.()
    }
  }, [isPresent])

  return (
    <motion.li
      className={clsx(
        'flex w-max items-center shadow px-4 py-3 rounded border transition-colors duration-100 min-w-[260px] text-sm pointer-events-auto',
        notificationStyleVariants[type]
      )}
      initial="initial"
      animate="animate"
      exit="exit"
      layout="position"
      custom={position}
      variants={!prefersReducedMotion ? motionVariants : {}}
      onMouseEnter={onMouseEnter}
      onMouseLeave={onMouseLeave}
    >
      <div className="flex gap-2 items-center">
        {notificationIcons[type]}
        <span className="max-w-sm font-medium">{message}</span>
      </div>

      <div className="pl-4 ml-auto">
        <button
          onClick={handleDismiss}
          className={clsx(
            'p-1 rounded transition-colors duration-100',
            closeButtonStyleVariants[type]
          )}
        >
          <Cross2Icon />
        </button>
      </div>
    </motion.li>
  )
}

마지막으로, Next 와 같은 루트 레벨에서 어셈블리를 내보냅니다.js_app.tsx포장지
import '@styles/globals.css'
import type { AppProps } from 'next/app'
import { Provider } from 'react-redux'

import { Notifications } from '@features/notifications/components/Notifications'
import { store } from '@redux/store'

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <>
      <Provider store={store}>
        <Component {...pageProps} />

        <Notifications />
      </Provider>
    </>
  )
}

export default MyApp

현재 상태 영화에서 만든 Redux 작업을 스케줄링할 수 있습니다.addNotification 및 알림을 제공합니다.👍
// Any component

import { addNotification } from '@features/notifications/notification.slice'
import { useAppDispatch } from '@redux/hooks'

export const Component = () => {
  const dispatch = useAppDispatch()

  return (
    <button
      onClick={() =>
        dispatch(
          addNotification({
            message: 'Hello world!',
            type: 'info',
            onClose: () => console.log('I was closed'),
            autoHideDuration: 6000,
          })
        )
      }
    >
      Render notification
    </button>
  )
}

데모: Here
Github 저장소: Here
필기
알림 애니메이션의 영감https://chakra-ui.com/docs/feedback/toast

좋은 웹페이지 즐겨찾기