Creating a TimeoutMessage Component for Temporary User Messages in React

September 11, 2023

3 min read

Creating a TimeoutMessage Component for Temporary User Messages in React
Watch on YouTube

There is a useful UX pattern of displaying a temporary message in response to a user's action. Recently, I had to add such a timeout message to my project and I thought it might be beneficial to package it into an abstract component and share it with you.

How To Use the TimeoutMessage Component

Here's how you can use it to display a temporary message after a user updates their profile.

Update Profile Message
Update Profile Message

If one of the values in the deps array has changed, the component will call the render function and display the message for five seconds.

<TimeoutMessage
  deps={[isAnonymous, name, country]}
  timeout={5000}
  render={() => <UpdateProfileMessage />}
/>

TimeoutMessage Component

The TimeoutMessage component is a simple wrapper around the useTimeoutState and useEffectOnDependencyChange hooks. Instead of using those hooks directly, we achieve a more declarative and compact code using the TimeoutMessage component.

import { DependencyList, ReactNode } from "react"
import { useEffectOnDependencyChange } from "../hooks/useEffectOnDependencyChange"
import { useTimeoutState } from "../hooks/useTimeoutState"

interface TimeoutMessageProps {
  render: () => ReactNode
  timeout: number
  deps: DependencyList
}

export const TimeoutMessage = ({
  render,
  timeout,
  deps,
}: TimeoutMessageProps) => {
  const [shouldShowMessage, setShouldShowMessage] = useTimeoutState(
    false,
    timeout
  )

  useEffectOnDependencyChange(() => {
    setShouldShowMessage(true)
  }, deps)

  return shouldShowMessage ? <>{render()}</> : null
}

useTimeoutState Hook

The useTimeoutState hook is similar to the useState hook, but it resets the state to the default value after a given timeout.

import { useEffect, useState } from "react"

export const useTimeoutState = <T,>(
  defaultValue: T,
  timeoutDuration: number
) => {
  const [state, setState] = useState(defaultValue)

  useEffect(() => {
    if (state !== defaultValue) {
      const timeout = setTimeout(() => {
        setState(defaultValue)
      }, timeoutDuration)

      return () => clearTimeout(timeout)
    }
  }, [state, defaultValue, setState, timeoutDuration])

  return [state, setState] as const
}

In our TimeoutMessage component, we use useTimeoutState to monitor the message visibility. We set shouldShowMessage to true when the deps change, and the useTimeoutState hook will reset it back to false after the specified timeout.

useEffectOnDependencyChange Hook

The useEffectOnDependencyChange hook is the same as the useEffect hook. However, it only calls the effect function when one of the values in the deps array has changed, and will not call it on the initial render.

import { DependencyList, useEffect, useRef } from "react"

export const useEffectOnDependencyChange = (
  effect: () => void,
  deps: DependencyList
) => {
  const prevDeps = useRef(deps)
  useEffect(() => {
    const hasDepsChanged = !prevDeps.current.every((dep, i) => dep === deps[i])
    if (hasDepsChanged) {
      effect()
      prevDeps.current = deps
    }
  }, deps)
}