Skip to content

Instantly share code, notes, and snippets.

@TheMcMurder
Last active August 23, 2021 16:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TheMcMurder/ad282784ba49831eb565746efaf1545a to your computer and use it in GitHub Desktop.
Save TheMcMurder/ad282784ba49831eb565746efaf1545a to your computer and use it in GitHub Desktop.
One potential way to handle a snackbar queue using react and rxjs

notification service

A centralized API for dispatching notifications through Snackbars and Banners.

API

triggerSnackbar

Triggers a Snackbar that is displayed to the user.

Snackbars inform users of a process that Workfront has performed or will perform. They appear temporarily, should not interrupt the user experience, and don't require user input to disappear.

function triggerSnackbar({
  message: string,
  action?: Function,
  actionText?: string
  timeout?: number
}) : Function
  • Only one snackbar will be displayed at a time
  • Additional snackbars will be queued and displayed after the previous has cleared
  • Snackbars don't need any buttons but can contain a single action
  • The timeout for the snackbar is calculated as .1 second per character of the message and action text, with a minimum of 4 seconds and a maximum of 10 seconds
  • The timeout stops when the user hovers over the snackbar area
  • The timeout restarts from zero when the user hovers away from the snackbar
  • The Snackbar is removed immediately once the action has been triggered
import { triggerSnackbar } from '@company/notification-service'

export function MyComponent() {

  const executeSomeAction = () => {
    doTheThing.then((result) => {
      triggerSnackbar({
        message: 'The thing was done'
      })
    })
  }

  return (/* ... */)
}
import React, { useState, useEffect, useRef } from 'react'
import { BehaviorSubject } from 'rxjs'
import Snackbar from './Snackbar'
import { cls } from './classNames'
import { SwitchTransition, CSSTransition } from 'react-transition-group'
import styles from './SnackbarAnimation.css'
import { clamp } from 'lodash'
const snackbarStream = new BehaviorSubject()
let snackbarId = 1
export const triggerSnackbar = ({ message, action, actionText }) => {
if (!message) throw new Error('triggerSnackbar requires a toast.message')
if (actionText && !action)
throw new Error(
'triggerSnackbar was called with toast.actionText but no corresponding toast.action'
)
if (action && !actionText)
throw new Error(
'triggerSnackbar was called with toast.action but no corresponding toast.actionText'
)
const id = snackbarId++
snackbarStream.next({ id, message, action, actionText })
return function clearSnackbar() {
snackbarStream.next({ id })
}
}
const TIMEOUT = {
MIN: 4000,
MAX: 10000,
CALCULATE: (msgLen, atLen = 0) => (msgLen + atLen) * 100,
}
export default function SnackbarRoot() {
const [snackbars, setSnackbars] = useState([])
const [timerPause, setTimerPause] = useState(false)
const timer = useRef()
const [currentSnackbar] = snackbars
useEffect(() => {
const sub = snackbarStream.subscribe((snackbar) => {
if (snackbar) {
setSnackbars((s) => {
if (!snackbar.message) {
return s.filter(({ id }) => id !== snackbar.id)
}
return s.concat({
...snackbar,
// 1. Default time is calculated based on text length (100ms per character)
// 2. Min of 4 seconds
// 3. Max of 10 seconds
// 4. Developers can provide a timeout within the min/max
timeout: clamp(
snackbar.timeout ||
TIMEOUT.CALCULATE(
snackbar.message.length,
snackbar.actionText?.length
),
TIMEOUT.MIN,
TIMEOUT.MAX
),
})
})
}
})
return () => {
sub.unsubscribe()
}
}, [])
const cancelTimer = () => {
clearTimeout(timer.current)
timer.current = undefined
}
const shiftSnackbars = React.useCallback(() => {
setSnackbars((queuedSnackbars) => {
if (!queuedSnackbars.length) {
cancelTimer()
}
return queuedSnackbars.slice(1)
})
}, [setSnackbars])
useEffect(() => {
if (timerPause) {
cancelTimer()
} else if (snackbars.length && !timer.current) {
timer.current = setTimeout(shiftSnackbars, currentSnackbar.timeout)
}
return () => {
cancelTimer()
}
}, [snackbars, currentSnackbar, timerPause, shiftSnackbars])
const key = currentSnackbar?.id || false
return (
<SwitchTransition mode="out-in">
<CSSTransition
key={key}
addEndListener={(node, done) => {
node.addEventListener('transitionend', done, false)
}}
classNames={{ ...styles }}
>
<div
className={cls('fixed bottom-0 left-0 p-4')}
aria-live="polite"
onMouseEnter={() => setTimerPause(true)}
onMouseLeave={() => setTimerPause(false)}
onClick={(e) => {
// do it this way because a Snackbar may omit an action
if (e.target.tagName === 'BUTTON') shiftSnackbars()
}}
>
{key && <Snackbar {...currentSnackbar} />}
</div>
</CSSTransition>
</SwitchTransition>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment