Приветствую вас, разработчики!
Я хотел бы показать вам свой вариант создания простой глобальной системы уведомлений с помощью superstate и React.
У нас есть одна дополнительная, неявная цель: построить что-то с удовлетворительной эргономикой и удобством для разработчиков.
Без лишних слов,
Предварительные условия
Я собираюсь создать совершенно новое приложение create-react-app
с TypeScript:
yarn create react-app superstate-notifications --template typescript
Обратите внимание, что я использую yarn
, но вы можете подражать моим командам, используя npm
.
Когда все готово, давайте переместим наш рабочий каталог в приложение superstate-notifications
, которое мы только что создали:
cd superstate-notifications
А затем установим superstate:
yarn add @superstate/core
Круто. Теперь у нас есть готовый проект.
Что такое superstate?
Вкратце, superstate — это библиотека управления микросостояниями для JavaScript-приложений. Несмотря на нюансы, вы можете думать о ней как об альтернативном решении для Redux или Zustand.
Она была разработана с учетом пожеланий разработчиков и поставляется в комплекте с мощной и удобной системой черновиков, чтобы сделать нашу жизнь проще и менее повторяющейся.
Начало работы
Теперь, когда у вас есть рабочий проект, давайте создадим файл notifications.tsx
в src/
и загрузим состояние наших уведомлений:
import { superstate } from '@superstate/core'
const notifications = superstate([])
Обратите внимание на []
в superstate()
. Это начальное значение вашего состояния. Это как если бы вы набрали:
const notifications = []
За исключением того, что вы обернули пустой массив внутри суперсостояния, и это дает нам полномочия.
Создание и уничтожение уведомлений
Следующий шаг — создание двух самых важных функций функции уведомлений: notify
и destroy
. Соответственно, одна из них предназначена для выпуска новых уведомлений, а другая — для их уничтожения.
Вот что я придумал:
function notify(message: string) {
const id = Math.random().toString()
notifications.set((prev) => [...prev, { id, message }])
}
function destroy(id: string) {
notifications.set((prev) => prev.filter((p) => p.id !== id))
}
Функция notify
.
Она ожидает получить аргумент message
(типа string
). Это сообщение — то, что увидит пользователь, когда появится уведомление.
Также эта функция объявляет переменную id
и присваивает ей Math.random().toString()
. Это просто потому, что мы хотим, чтобы наша система поддерживала несколько уведомлений одновременно, и у нас должен быть способ отличить одно уведомление от другого — id
является таким способом.
Более того, функция notify
вызывает .set()
из нашего объекта notifications
. Если вы прокрутите немного вверх, то заметите, что объект notifications
является нашей переменной superstate()
, поэтому .set()
— это функция, возвращаемая из него.
Сначала это может показаться сложным, но все, что мы делаем, это передаем в .set()
функцию, которая возвращает, как должен выглядеть список уведомлений после того, как мы выпустим новое уведомление.
prev
— это предыдущее значение notifications
. Изначально значение notifications
равно []
(пустой массив), но по мере того, как мы начнем выпускать уведомления, этот массив будет расти, поэтому prev
гарантирует, что мы добавляем новые уведомления, а не заменяем их.
Посмотрите, что мы делаем снова:
notifications.set((prev) => [...prev, { id, message }])
Это означает, что следующее значение notifications
— это прежние уведомления плюс новое, которое представлено объектом со свойствами id
и message
.
Функция destroy
.
Здесь мы говорим, что следующим значением notifications
будут все уведомления, кроме того, которое соответствует указанному id
, переданному через аргумент функции destroy
:
notifications.set((prev) => prev.filter((p) => p.id !== id))
Уведомления о рендеринге
Теперь в этом же файле notifications.tsx
создадим рендерер уведомлений. Его работа очень важна: отображение уведомлений пользователю.
Вот его загрузка:
export function NotificationsRenderer() {
useSuperState(notifications)
return null
}
Подождите, что? Откуда взялась эта функция useSuperState()
?
Да, я не упоминал о ней до сих пор. Намеренно. Чтобы интегрировать superstate в React, вам придется установить дополнительную зависимость:
yarn add @superstate/react
И импортировать ее в ваш файл notifications.tsx
:
import { useSuperState } from '@superstate/react'
Хук useSuperState
перерисовывает наш компонент (NotificationsRenderer) каждый раз при изменении переданного ему состояния. В нашем контексте, это «состояние, переданное ему» относится к notifications
.
Вот что я придумал, чтобы сделать рендерер полностью функциональным:
export function NotificationsRenderer() {
useSuperState(notifications)
if (!notifications.now().length) {
return null
}
return (
<div>
{notifications.now().map((n) => {
return (
<div key={n.id}>
<p>{n.message}</p>
<button onClick={() => destroy(n.id)}>
Destroy
</button>
</div>
)
})}
</div>
)
}
Давайте разделим это на части:
if (!notifications.now().length) {
return null
}
Приведенное выше if
гарантирует, что при отсутствии уведомлений ничего не будет отображаться. Обратите внимание на метод now()
— он возвращает текущее значение массива notifications
. Условие гласит, что если в списке notifications
нет элементов, то мы хотим отобразить null
.
{notifications.now().map((n) => {
Строка выше будет перебирать каждый элемент в массиве notifications
и возвращать что-то. В нашем контексте, для каждого уведомления будет отрисовано что-то. Обратите внимание, что now()
снова присутствует.
return (
<div key={n.id}>
<p>{n.message}</p>
<button onClick={() => destroy(n.id)}>
Destroy
</button>
</div>
)
Строки выше относятся к фактическому элементу уведомления, который будет отрисован в браузере и показан пользователю.
В качестве последнего кусочка головоломки рендеринга откроем ./src/App.tsx
и очистим возвращаемый компонент, чтобы он выглядел примерно так:
export default function App() {
return ()
}
Теперь, когда в доме чисто, мы можем приступить к рендерингу:
import { NotificationsRenderer } from './notifications'
export default function App() {
return (
<div>
<NotificationsRenderer />
<button>Give me a notification!</button>
</div>
)
}
Выдача уведомлений
Возможно, вы заметили, что мы создали кнопку Give me a notification!
в разделе выше, но ничего с ней не сделали. Ну, пока.
Давайте сделаем так, чтобы она выдавала нам уведомление при каждом нажатии на нее:
<button onClick={() => notify('Hello world!')}>
Give me a notification!
</button>
Функция notify
не будет работать сразу. Сначала нам нужно экспортировать ее. Вернитесь к файлу notifications.tsx
и экспортируйте обе функции notify
и destroy
, добавив ключевое слово export
перед ключевым словом function
:
export function notify(message: string) {
const id = Math.random().toString()
notifications.set((prev) => [...prev, { id, message }])
}
export function destroy(id: string) {
notifications.set((prev) => prev.filter((p) => p.id !== id))
}
Теперь, в App.tsx
, вы сможете импортировать их:
import {
notify,
destroy,
NotificationsRenderer,
} from './notifications'
И бум! Сохраните все ваши файлы и идите в браузер играть с вашей свежей системой уведомлений 🙂
Подведение итогов
Ваш финальный notifications.tsx
должен выглядеть следующим образом:
import { superstate } from '@superstate/core'
import { useSuperState } from '@superstate/react'
const notifications = superstate([])
export function notify(message: string) {
const id = Math.random().toString()
notifications.set((prev) => [...prev, { id, message }])
}
export function destroy(id: string) {
notifications.set((prev) => prev.filter((p) => p.id !== id))
}
export function NotificationsRenderer() {
useSuperState(notifications)
if (!notifications.now().length) {
return null
}
return (
<div>
{notifications.now().map((n) => {
return (
<div key={n.id}>
<p>{n.message}</p>
<button onClick={() => destroy(n.id)}>
Destroy
</button>
</div>
)
})}
</div>
)
}
И ваш App.tsx
:
import {
notify,
destroy,
NotificationsRenderer,
} from './notifications'
export default function App() {
return (
<div>
<NotificationsRenderer />
<button onClick={() => notify('Hello world!')}>
Give me a notification!
</button>
</div>
)
}
Вы можете посмотреть более сложный пример на StackBlitz:
Заключительные мысли
Это довольно базовая система уведомлений, но довольно мощная и интуитивно понятная. Теперь, чтобы отправлять уведомления в вашем приложении, все, что вам нужно сделать, это вызвать функцию notify()
, которую вы создали сами, из любого места вашего приложения, включая код, не относящийся к React, и получать удовольствие, потому что все будет работать.
А теперь идите и повеселитесь, и не стесняйтесь обращаться с любыми вопросами или отзывами! d(^_^)z