Sep 5, 2019

Custom Hooks: useTransientState

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:

$('#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.

import * as React from 'react';
import * as import ReactDOMReactDOM from 'react-dom';
import { function createRoot(container: Container, options?: RootOptions): Root
createRoot lets you create a root to display React components inside a browser DOM node.
@see{@link https://react.dev/reference/react-dom/client/createRoot API Reference for `createRoot`}
createRoot
} from 'react-dom/client';
const const useTransientState: <StateValue extends unknown>(steadyState: StateValue, restorationTime?: number) => readonly [StateValue, (newValue: StateValue) => void]useTransientState = <function (type parameter) StateValue in <StateValue extends unknown>(steadyState: StateValue, restorationTime?: number): readonly [StateValue, (newValue: StateValue) => void]StateValue extends unknown>( steadyState: StateValue extends unknownsteadyState: function (type parameter) StateValue in <StateValue extends unknown>(steadyState: StateValue, restorationTime?: number): readonly [StateValue, (newValue: StateValue) => void]StateValue, restorationTime: numberrestorationTime = 2000 ) => { const [const state: StateValue extends unknownstate, const setState: React.Dispatch<React.SetStateAction<StateValue>>setState] = React.function React.useState<StateValue>(initialState: StateValue | (() => StateValue)): [StateValue, React.Dispatch<React.SetStateAction<StateValue>>] (+1 overload)
Returns a stateful value, and a function to update it.
@version16.8.0@see{@link https://react.dev/reference/react/useState}
useState
(steadyState: StateValue extends unknownsteadyState);
const const setTemporaryState: (newValue: StateValue) => voidsetTemporaryState = React.function React.useCallback<(newValue: StateValue) => void>(callback: (newValue: StateValue) => void, deps: React.DependencyList): (newValue: StateValue) => void
`useCallback` will return a memoized version of the callback that only changes if one of the `inputs` has changed.
@version16.8.0@see{@link https://react.dev/reference/react/useCallback}
useCallback
(function function (local function) setTemporaryState(newValue: StateValue): voidsetTemporaryState(newValue: StateValue extends unknownnewValue: function (type parameter) StateValue in <StateValue extends unknown>(steadyState: StateValue, restorationTime?: number): readonly [StateValue, (newValue: StateValue) => void]StateValue) {
const setState: (value: React.SetStateAction<StateValue>) => voidsetState(newValue: StateValue extends unknownnewValue); }, []); React.function React.useEffect(effect: React.EffectCallback, deps?: React.DependencyList): void
Accepts a function that contains imperative, possibly effectful code.
@parameffect Imperative function that can return a cleanup function@paramdeps If present, effect will only activate if the values in the list change.@version16.8.0@see{@link https://react.dev/reference/react/useEffect}
useEffect
(() => {
if (const state: StateValue extends unknownstate !== steadyState: StateValue extends unknownsteadyState && restorationTime: numberrestorationTime) { const const timeoutId: NodeJS.TimeouttimeoutId = function setTimeout<[]>(callback: () => void, ms?: number): NodeJS.Timeout (+3 overloads)
Schedules execution of a one-time `callback` after `delay` milliseconds. The `callback` will likely not be invoked in precisely `delay` milliseconds. Node.js makes no guarantees about the exact timing of when callbacks will fire, nor of their ordering. The callback will be called as close as possible to the time specified. When `delay` is larger than `2147483647` or less than `1`, the `delay` will be set to `1`. Non-integer delays are truncated to an integer. If `callback` is not a function, a `TypeError` will be thrown. This method has a custom variant for promises that is available using `timersPromises.setTimeout()`.
@sincev0.0.1@paramcallback The function to call when the timer elapses.@paramdelay The number of milliseconds to wait before calling the `callback`.@paramargs Optional arguments to pass when the `callback` is called.@returnfor use with {@link clearTimeout}
setTimeout
(() => const setState: (value: React.SetStateAction<StateValue>) => voidsetState(steadyState: StateValue extends unknownsteadyState), restorationTime: numberrestorationTime);
return () => function clearTimeout(timeoutId: NodeJS.Timeout | string | number | undefined): void (+2 overloads)
Cancels a `Timeout` object created by `setTimeout()`.
@sincev0.0.1@paramtimeout A `Timeout` object as returned by {@link setTimeout} or the `primitive` of the `Timeout` object as a string or a number.
clearTimeout
(const timeoutId: NodeJS.TimeouttimeoutId);
} }, [const state: StateValue extends unknownstate, steadyState: StateValue extends unknownsteadyState, restorationTime: numberrestorationTime]); return [const state: StateValue extends unknownstate, const setTemporaryState: (newValue: StateValue) => voidsetTemporaryState] as type const = readonly [StateValue, (newValue: StateValue) => void]const; }; const const TemporaryMessage: () => React.JSX.ElementTemporaryMessage = () => { const [const show: booleanshow, const setShow: (newValue: boolean) => voidsetShow] = const useTransientState: <boolean>(steadyState: boolean, restorationTime?: number) => readonly [boolean, (newValue: boolean) => void]useTransientState(false, 1000); return ( <JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div> <JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() => const setShow: (newValue: boolean) => voidsetShow(true)}>Show Message</JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button> {const show: booleanshow && <JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>I only appear a while!</JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>} </JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div> ); }; function createRoot(container: Container, options?: RootOptions): Root
createRoot lets you create a root to display React components inside a browser DOM node.
@see{@link https://react.dev/reference/react-dom/client/createRoot API Reference for `createRoot`}
createRoot
(var document: Document
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)
document
.Document.getElementById(elementId: string): HTMLElement | null
Returns a reference to the first object with the specified value of the ID attribute.
@paramelementId String that specifies the ID value.
getElementById
('root')!).Root.render(children: React.ReactNode): voidrender(<const TemporaryMessage: () => React.JSX.ElementTemporaryMessage />);
  • useCallback hook is used because I want to make the return stateSetter callback identity is always the same, just like useState. As everyone using React hooks know useState, I want to make this custom hook to be like useState as much as possible.
  • The effect hook will be run each time 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 each time the setTemporaryState callback is invoked.

To have that reset timeout behavior, we can create another state to make sure the effect is run each time setTemporaryState is invoked.

import * as React from 'react';
import * as import ReactDOMReactDOM from 'react-dom';
import { function createRoot(container: Container, options?: RootOptions): Root
createRoot lets you create a root to display React components inside a browser DOM node.
@see{@link https://react.dev/reference/react-dom/client/createRoot API Reference for `createRoot`}
createRoot
} from 'react-dom/client';
const const useTransientState: <StateValue extends unknown>(steadyState: StateValue, restorationTime?: number) => readonly [StateValue, (newValue: StateValue) => void]useTransientState = <function (type parameter) StateValue in <StateValue extends unknown>(steadyState: StateValue, restorationTime?: number): readonly [StateValue, (newValue: StateValue) => void]StateValue extends unknown>( steadyState: StateValue extends unknownsteadyState: function (type parameter) StateValue in <StateValue extends unknown>(steadyState: StateValue, restorationTime?: number): readonly [StateValue, (newValue: StateValue) => void]StateValue, restorationTime: numberrestorationTime = 2000 ) => { const [const state: StateValue extends unknownstate, const setState: React.Dispatch<React.SetStateAction<StateValue>>setState] = React.function React.useState<StateValue>(initialState: StateValue | (() => StateValue)): [StateValue, React.Dispatch<React.SetStateAction<StateValue>>] (+1 overload)
Returns a stateful value, and a function to update it.
@version16.8.0@see{@link https://react.dev/reference/react/useState}
useState
(steadyState: StateValue extends unknownsteadyState);
const [const calledTimes: numbercalledTimes, const setCallTimes: React.Dispatch<React.SetStateAction<number>>setCallTimes] = React.function React.useState<number>(initialState: number | (() => number)): [number, React.Dispatch<React.SetStateAction<number>>] (+1 overload)
Returns a stateful value, and a function to update it.
@version16.8.0@see{@link https://react.dev/reference/react/useState}
useState
(0); // [!code highlight]
const const setTemporaryState: (newValue: StateValue) => voidsetTemporaryState = React.function React.useCallback<(newValue: StateValue) => void>(callback: (newValue: StateValue) => void, deps: React.DependencyList): (newValue: StateValue) => void
`useCallback` will return a memoized version of the callback that only changes if one of the `inputs` has changed.
@version16.8.0@see{@link https://react.dev/reference/react/useCallback}
useCallback
(function function (local function) setTemporaryState(newValue: StateValue): voidsetTemporaryState(newValue: StateValue extends unknownnewValue: function (type parameter) StateValue in <StateValue extends unknown>(steadyState: StateValue, restorationTime?: number): readonly [StateValue, (newValue: StateValue) => void]StateValue) {
const setState: (value: React.SetStateAction<StateValue>) => voidsetState(newValue: StateValue extends unknownnewValue); const setCallTimes: (value: React.SetStateAction<number>) => voidsetCallTimes((t: numbert) => t: numbert + 1); // [!code highlight] }, []); React.function React.useEffect(effect: React.EffectCallback, deps?: React.DependencyList): void
Accepts a function that contains imperative, possibly effectful code.
@parameffect Imperative function that can return a cleanup function@paramdeps If present, effect will only activate if the values in the list change.@version16.8.0@see{@link https://react.dev/reference/react/useEffect}
useEffect
(() => {
if (const state: StateValue extends unknownstate !== steadyState: StateValue extends unknownsteadyState && restorationTime: numberrestorationTime) { const const timeoutId: NodeJS.TimeouttimeoutId = function setTimeout<[]>(callback: () => void, ms?: number): NodeJS.Timeout (+3 overloads)
Schedules execution of a one-time `callback` after `delay` milliseconds. The `callback` will likely not be invoked in precisely `delay` milliseconds. Node.js makes no guarantees about the exact timing of when callbacks will fire, nor of their ordering. The callback will be called as close as possible to the time specified. When `delay` is larger than `2147483647` or less than `1`, the `delay` will be set to `1`. Non-integer delays are truncated to an integer. If `callback` is not a function, a `TypeError` will be thrown. This method has a custom variant for promises that is available using `timersPromises.setTimeout()`.
@sincev0.0.1@paramcallback The function to call when the timer elapses.@paramdelay The number of milliseconds to wait before calling the `callback`.@paramargs Optional arguments to pass when the `callback` is called.@returnfor use with {@link clearTimeout}
setTimeout
(() => const setState: (value: React.SetStateAction<StateValue>) => voidsetState(steadyState: StateValue extends unknownsteadyState), restorationTime: numberrestorationTime);
return () => function clearTimeout(timeoutId: NodeJS.Timeout | string | number | undefined): void (+2 overloads)
Cancels a `Timeout` object created by `setTimeout()`.
@sincev0.0.1@paramtimeout A `Timeout` object as returned by {@link setTimeout} or the `primitive` of the `Timeout` object as a string or a number.
clearTimeout
(const timeoutId: NodeJS.TimeouttimeoutId);
} }, [const state: StateValue extends unknownstate, steadyState: StateValue extends unknownsteadyState, restorationTime: numberrestorationTime, const calledTimes: numbercalledTimes]); // [!code highlight] return [const state: StateValue extends unknownstate, const setTemporaryState: (newValue: StateValue) => voidsetTemporaryState] as type const = readonly [StateValue, (newValue: StateValue) => void]const; }; const const TemporaryMessage: () => React.JSX.ElementTemporaryMessage = () => { const [const show: booleanshow, const setShow: (newValue: boolean) => voidsetShow] = const useTransientState: <boolean>(steadyState: boolean, restorationTime?: number) => readonly [boolean, (newValue: boolean) => void]useTransientState(false, 1000); return ( <JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div> <JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() => const setShow: (newValue: boolean) => voidsetShow(true)}>Show Message</JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button> {const show: booleanshow && <JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>I only appear a while!</JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>} </JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div> ); }; function createRoot(container: Container, options?: RootOptions): Root
createRoot lets you create a root to display React components inside a browser DOM node.
@see{@link https://react.dev/reference/react-dom/client/createRoot API Reference for `createRoot`}
createRoot
(var document: Document
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)
document
.Document.getElementById(elementId: string): HTMLElement | null
Returns a reference to the first object with the specified value of the ID attribute.
@paramelementId String that specifies the ID value.
getElementById
('root')!).Root.render(children: React.ReactNode): voidrender(<const TemporaryMessage: () => React.JSX.ElementTemporaryMessage />);

Thanks for reading!

Love what you're reading? Sign up for my newsletter and stay up-to-date with my latest contents and projects.

    I won't send you spam or use it for other purposes.

    Unsubscribe at any time.