One of the common pattern in UI is to show something in a short period then hide it, e.g. using Snackbars for notification or popup for brief message.
In good old jquery times where things are imperative, we usually do something like this:
js
$('#element').toast('show', { delay: 500 });
js
$('#element').toast('show', { delay: 500 });
But how do we model this kind of behavior in React?
I was thinking about this question when I want to show some brief message while developing a nonsense game (whose UI is mostly a copy from Wendy’s RxJS Mamak) to learn xstate.
After searching for some library and see their API and I have an epiphany: what I want essentially is a state that will auto restore to its steady state after some delay everytime you change it.
And here I present to you the custom hook: useTransientState
.
tsx
import * asReactDOM from 'react-dom';import {createRoot } from 'react-dom/client';constuseTransientState = <StateValue extends unknown>(steadyState :StateValue ,restorationTime = 2000) => {const [state ,setState ] =React .useState (steadyState );constsetTemporaryState =React .useCallback (functionsetTemporaryState (newValue :StateValue ) {setState (newValue );}, []);React .useEffect (() => {if (state !==steadyState &&restorationTime ) {consttimeoutId =setTimeout (() =>setState (steadyState ),restorationTime );return () =>clearTimeout (timeoutId );}}, [state ,steadyState ,restorationTime ]);return [state ,setTemporaryState ] asconst ;};constTemporaryMessage = () => {const [show ,setShow ] =useTransientState (false, 1000);return (<div ><button onClick ={() =>setShow (true)}>Show Message</button >{show && <p >I only appear a while!</p >}</div >);};createRoot (document .getElementById ('root')!).render (<TemporaryMessage />);
tsx
import * asReactDOM from 'react-dom';import {createRoot } from 'react-dom/client';constuseTransientState = <StateValue extends unknown>(steadyState :StateValue ,restorationTime = 2000) => {const [state ,setState ] =React .useState (steadyState );constsetTemporaryState =React .useCallback (functionsetTemporaryState (newValue :StateValue ) {setState (newValue );}, []);React .useEffect (() => {if (state !==steadyState &&restorationTime ) {consttimeoutId =setTimeout (() =>setState (steadyState ),restorationTime );return () =>clearTimeout (timeoutId );}}, [state ,steadyState ,restorationTime ]);return [state ,setTemporaryState ] asconst ;};constTemporaryMessage = () => {const [show ,setShow ] =useTransientState (false, 1000);return (<div ><button onClick ={() =>setShow (true)}>Show Message</button >{show && <p >I only appear a while!</p >}</div >);};createRoot (document .getElementById ('root')!).render (<TemporaryMessage />);
tsx
import * asReactDOM from 'react-dom';import {createRoot } from 'react-dom/client';constuseTransientState = <StateValue extends unknown>(steadyState :StateValue ,restorationTime = 2000) => {const [state ,setState ] =React .useState (steadyState );constsetTemporaryState =React .useCallback (functionsetTemporaryState (newValue :StateValue ) {setState (newValue );}, []);React .useEffect (() => {if (state !==steadyState &&restorationTime ) {consttimeoutId =setTimeout (() =>setState (steadyState ),restorationTime );return () =>clearTimeout (timeoutId );}}, [state ,steadyState ,restorationTime ]);return [state ,setTemporaryState ] asconst ;};constTemporaryMessage = () => {const [show ,setShow ] =useTransientState (false, 1000);return (<div ><button onClick ={() =>setShow (true)}>Show Message</button >{show && <p >I only appear a while!</p >}</div >);};createRoot (document .getElementById ('root')!).render (<TemporaryMessage />);
tsx
import * asReactDOM from 'react-dom';import {createRoot } from 'react-dom/client';constuseTransientState = <StateValue extends unknown>(steadyState :StateValue ,restorationTime = 2000) => {const [state ,setState ] =React .useState (steadyState );constsetTemporaryState =React .useCallback (functionsetTemporaryState (newValue :StateValue ) {setState (newValue );}, []);React .useEffect (() => {if (state !==steadyState &&restorationTime ) {consttimeoutId =setTimeout (() =>setState (steadyState ),restorationTime );return () =>clearTimeout (timeoutId );}}, [state ,steadyState ,restorationTime ]);return [state ,setTemporaryState ] asconst ;};constTemporaryMessage = () => {const [show ,setShow ] =useTransientState (false, 1000);return (<div ><button onClick ={() =>setShow (true)}>Show Message</button >{show && <p >I only appear a while!</p >}</div >);};createRoot (document .getElementById ('root')!).render (<TemporaryMessage />);
useCallback
hook is used because I want to make the return stateSetter callback identity is always the same, just likeuseState
. As everyone using React hooks knowuseState
, I want to make this custom hook to be likeuseState
as much as possible.- The effect hook will be run everytime the state is changed. It will set up a timeout which will set the state back to
steadState
.
Improvement
While writing this blog I realize there is a problem with the code above, which is the timeout is not reset when you update the state again.
You can reproduce this behavior if you click on the button above multiple times, and the message will auto hide 1 seconds after the first time you click it. This is because the effect hooks will not rerun if state
, steadyState
, and restorationTime
is unchanged.
This may or may not be what you want, but for me it’s unintuitive. The more intuitive behavior would be the timeout will be reset everytime the setTemporaryState
callback is invoked.
To have that reset timeout behavior, we can create another state to make sure the effect is run everytime setTemporaryState
is invoked.
tsx
import * asReactDOM from 'react-dom';import {createRoot } from 'react-dom/client';constuseTransientState = <StateValue extends unknown>(steadyState :StateValue ,restorationTime = 2000) => {const [state ,setState ] =React .useState (steadyState );const [calledTimes ,setCallTimes ] =React .useState (0);constsetTemporaryState =React .useCallback (functionsetTemporaryState (newValue :StateValue ) {setState (newValue );setCallTimes ((t ) =>t + 1);}, []);React .useEffect (() => {if (state !==steadyState &&restorationTime ) {consttimeoutId =setTimeout (() =>setState (steadyState ),restorationTime );return () =>clearTimeout (timeoutId );}}, [state ,steadyState ,restorationTime ,calledTimes ]);return [state ,setTemporaryState ] asconst ;};constTemporaryMessage = () => {const [show ,setShow ] =useTransientState (false, 1000);return (<div ><button onClick ={() =>setShow (true)}>Show Message</button >{show && <p >I only appear a while!</p >}</div >);};createRoot (document .getElementById ('root')!).render (<TemporaryMessage />);
tsx
import * asReactDOM from 'react-dom';import {createRoot } from 'react-dom/client';constuseTransientState = <StateValue extends unknown>(steadyState :StateValue ,restorationTime = 2000) => {const [state ,setState ] =React .useState (steadyState );const [calledTimes ,setCallTimes ] =React .useState (0);constsetTemporaryState =React .useCallback (functionsetTemporaryState (newValue :StateValue ) {setState (newValue );setCallTimes ((t ) =>t + 1);}, []);React .useEffect (() => {if (state !==steadyState &&restorationTime ) {consttimeoutId =setTimeout (() =>setState (steadyState ),restorationTime );return () =>clearTimeout (timeoutId );}}, [state ,steadyState ,restorationTime ,calledTimes ]);return [state ,setTemporaryState ] asconst ;};constTemporaryMessage = () => {const [show ,setShow ] =useTransientState (false, 1000);return (<div ><button onClick ={() =>setShow (true)}>Show Message</button >{show && <p >I only appear a while!</p >}</div >);};createRoot (document .getElementById ('root')!).render (<TemporaryMessage />);
tsx
import * asReactDOM from 'react-dom';import {createRoot } from 'react-dom/client';constuseTransientState = <StateValue extends unknown>(steadyState :StateValue ,restorationTime = 2000) => {const [state ,setState ] =React .useState (steadyState );const [calledTimes ,setCallTimes ] =React .useState (0);constsetTemporaryState =React .useCallback (functionsetTemporaryState (newValue :StateValue ) {setState (newValue );setCallTimes ((t ) =>t + 1);}, []);React .useEffect (() => {if (state !==steadyState &&restorationTime ) {consttimeoutId =setTimeout (() =>setState (steadyState ),restorationTime );return () =>clearTimeout (timeoutId );}}, [state ,steadyState ,restorationTime ,calledTimes ]);return [state ,setTemporaryState ] asconst ;};constTemporaryMessage = () => {const [show ,setShow ] =useTransientState (false, 1000);return (<div ><button onClick ={() =>setShow (true)}>Show Message</button >{show && <p >I only appear a while!</p >}</div >);};createRoot (document .getElementById ('root')!).render (<TemporaryMessage />);
tsx
import * asReactDOM from 'react-dom';import {createRoot } from 'react-dom/client';constuseTransientState = <StateValue extends unknown>(steadyState :StateValue ,restorationTime = 2000) => {const [state ,setState ] =React .useState (steadyState );const [calledTimes ,setCallTimes ] =React .useState (0);constsetTemporaryState =React .useCallback (functionsetTemporaryState (newValue :StateValue ) {setState (newValue );setCallTimes ((t ) =>t + 1);}, []);React .useEffect (() => {if (state !==steadyState &&restorationTime ) {consttimeoutId =setTimeout (() =>setState (steadyState ),restorationTime );return () =>clearTimeout (timeoutId );}}, [state ,steadyState ,restorationTime ,calledTimes ]);return [state ,setTemporaryState ] asconst ;};constTemporaryMessage = () => {const [show ,setShow ] =useTransientState (false, 1000);return (<div ><button onClick ={() =>setShow (true)}>Show Message</button >{show && <p >I only appear a while!</p >}</div >);};createRoot (document .getElementById ('root')!).render (<TemporaryMessage />);