Skip to main content

useLoading

Track loading state for async operations with automatic cleanup.

Signature

function useLoading(
  initialState?: boolean
): [boolean, WithLoading, (loading: boolean) => void]

type WithLoading = <T>(
  promise: undefined | Promise<T | void> | (() => Promise<T | void>)
) => Promise<T | void>
initialState
boolean
default:"false"
Initial loading state
[0]
boolean
Current loading state
[1]
WithLoading
Function to wrap promises with loading state tracking
[2]
(loading: boolean) => void
Manual loading state setter

Usage

import { useLoading } from '@proton/hooks';

function MyComponent() {
  const [loading, withLoading] = useLoading();

  const handleSubmit = async () => {
    await withLoading(async () => {
      await api.post('/endpoint', data);
    });
  };

  return <button disabled={loading}>Submit</button>;
}

Features

  • Automatic cleanup on unmount
  • Race condition protection (latest call wins)
  • Supports both promises and promise factories
  • Type-safe return values

useLoadingByKey

Track loading states for multiple concurrent operations using keys.

Signature

function useLoadingByKey(
  initialState?: LoadingByKey
): [LoadingByKey, WithLoadingByKey, (key: string, loading: boolean) => void]

type LoadingByKey = { [key: string]: boolean }
type WithLoadingByKey = <T>(
  key: string,
  promise: undefined | Promise<T | void> | (() => Promise<T | void>)
) => Promise<T | void>
initialState
LoadingByKey
default:"{}"
Initial loading states by key

Usage

import { useLoadingByKey } from '@proton/hooks';

function MultiActionComponent() {
  const [loadingMap, withLoadingByKey] = useLoadingByKey();

  const handleSave = () => withLoadingByKey('save', saveData());
  const handleDelete = () => withLoadingByKey('delete', deleteData());

  return (
    <>
      <button disabled={loadingMap.save}>Save</button>
      <button disabled={loadingMap.delete}>Delete</button>
    </>
  );
}

useControlled

Handle controlled/uncontrolled component patterns.

Signature

function useControlled<V>(
  controlled: V,
  defaultValue?: V
): readonly [V, (v: V) => void]
controlled
V
Controlled value (if undefined, component is uncontrolled)
defaultValue
V
Default value for uncontrolled mode

Usage

import useControlled from '@proton/hooks/useControlled';

interface InputProps {
  value?: string;
  defaultValue?: string;
  onChange?: (value: string) => void;
}

function Input({ value, defaultValue, onChange }: InputProps) {
  const [internalValue, setInternalValue] = useControlled(
    value,
    defaultValue
  );

  const handleChange = (newValue: string) => {
    setInternalValue(newValue);
    onChange?.(newValue);
  };

  return <input value={internalValue} onChange={handleChange} />;
}

useCombinedRefs

Combine multiple React refs into a single ref callback.

Signature

function useCombinedRefs<T>(
  ...refs: (Ref<T> | undefined)[]
): Ref<T>

Usage

import useCombinedRefs from '@proton/hooks/useCombinedRefs';
import { forwardRef, useRef } from 'react';

const MyComponent = forwardRef<HTMLDivElement>((props, ref) => {
  const internalRef = useRef<HTMLDivElement>(null);
  const combinedRef = useCombinedRefs(ref, internalRef);

  return <div ref={combinedRef}>Content</div>;
});

useInstance

Create a stable instance value that persists across renders.

Signature

function useInstance<T>(fn: () => T): T
fn
() => T
Factory function to create the instance (called only once)

Usage

import useInstance from '@proton/hooks/useInstance';

function MyComponent() {
  // EventManager is created once and reused
  const eventManager = useInstance(() => new EventManager());
  
  useEffect(() => {
    eventManager.subscribe('event', handler);
  }, [eventManager]);

  return <div>...</div>;
}

useIsMounted

Check if a component is currently mounted.

Signature

function useIsMounted(): () => boolean

Usage

import useIsMounted from '@proton/hooks/useIsMounted';

function MyComponent() {
  const isMounted = useIsMounted();

  const fetchData = async () => {
    const data = await api.fetch();
    
    // Only update state if component is still mounted
    if (isMounted()) {
      setState(data);
    }
  };

  return <div>...</div>;
}

useInterval

Declarative interval with automatic cleanup.

Signature

function useInterval(
  callback: () => void,
  delay: number | null
): void
callback
() => void
Function to call on each interval
delay
number | null
Interval delay in milliseconds (null to pause)

Usage

import useInterval from '@proton/hooks/useInterval';

function Timer() {
  const [count, setCount] = useState(0);

  useInterval(() => {
    setCount(c => c + 1);
  }, 1000);

  return <div>Count: {count}</div>;
}

useStateRef

Combine useState with useRef for synchronous access to state.

Signature

function useStateRef<S>(
  initialState: S | (() => S)
): [S, (value: S) => void, { current: S }]

Usage

import useStateRef from '@proton/hooks/useStateRef';

function MyComponent() {
  const [value, setValue, valueRef] = useStateRef(0);

  const handleAsync = async () => {
    await someAsyncOperation();
    // Access latest value synchronously
    console.log(valueRef.current);
  };

  return <button onClick={() => setValue(v => v + 1)}>{value}</button>;
}

usePrevious

Access the previous value of a variable.

Signature

function usePrevious<T>(value: T): T | undefined

Usage

import usePrevious from '@proton/hooks/usePrevious';

function MyComponent({ count }: { count: number }) {
  const prevCount = usePrevious(count);

  return (
    <div>
      Current: {count}, Previous: {prevCount}
    </div>
  );
}

useEffectOnce

Run an effect only once on component mount.

Signature

function useEffectOnce(effect: () => void | (() => void)): void

Usage

import useEffectOnce from '@proton/hooks/useEffectOnce';

function MyComponent() {
  useEffectOnce(() => {
    console.log('Mounted');
    
    return () => {
      console.log('Unmounted');
    };
  });

  return <div>...</div>;
}

useSearchParams

Read and write URL search parameters with React Router integration.

Signature

function useSearchParams(
  init?: ParsedSearchParams
): [ParsedSearchParams, SetSearchParams]

type ParsedSearchParams = Record<string, string | string[] | undefined>
type SetSearchParams = (
  nextInit: ParsedSearchParams | ((prev: ParsedSearchParams) => ParsedSearchParams)
) => void
init
ParsedSearchParams
Initial search parameters (applied on mount)

Usage

import useSearchParams from '@proton/hooks/useSearchParams';

function MyComponent() {
  const [searchParams] = useSearchParams();

  const userId = searchParams.userId;
  const filters = searchParams.filters; // can be string[]

  return <div>User ID: {userId}</div>;
}

useDateCountdown

Countdown timer to a target date with automatic updates.

Signature

function useDateCountdown(
  expiry: Date,
  options?: DateCountdownOptions
): DateCountdown

interface DateCountdown {
  diff: number;
  days: number;
  hours: number;
  minutes: number;
  seconds: number;
  expired: boolean;
}

interface DateCountdownOptions {
  interval?: number;
}
expiry
Date
required
Target date to count down to
options.interval
number
default:"1000"
Update interval in milliseconds

Usage

import useDateCountdown from '@proton/hooks/useDateCountdown';

function CountdownTimer() {
  const targetDate = new Date('2024-12-31T23:59:59');
  const countdown = useDateCountdown(targetDate);

  if (countdown.expired) {
    return <div>Time's up!</div>;
  }

  return (
    <div>
      {countdown.days}d {countdown.hours}h {countdown.minutes}m {countdown.seconds}s
    </div>
  );
}

useAsyncError

Throw errors asynchronously to be caught by Error Boundaries.

Signature

function useAsyncError(): (error: Error | null) => void

Usage

import useAsyncError from '@proton/hooks/useAsyncError';

function MyComponent() {
  const throwError = useAsyncError();

  const fetchData = async () => {
    try {
      await api.fetch();
    } catch (error) {
      // This will trigger the nearest Error Boundary
      throwError(error as Error);
    }
  };

  return <button onClick={fetchData}>Fetch Data</button>;
}
This hook allows you to throw errors from async operations so they can be caught by React Error Boundaries.

useStableLoading

Prevent loading state flickering with debounced transitions.

Signature

function useStableLoading(
  loadingCondition: boolean[] | boolean | (() => boolean),
  options?: StableLoadingOptions
): boolean

interface StableLoadingOptions {
  delay?: number;
  initialState?: boolean;
}
loadingCondition
boolean[] | boolean | (() => boolean)
required
Loading condition(s) to stabilize
options.delay
number
default:"300"
Delay in milliseconds before clearing loading state
options.initialState
boolean
default:"true"
Initial loading state

Usage

import useStableLoading from '@proton/hooks/useStableLoading';

function MyComponent() {
  const [loading] = useLoading();
  const stableLoading = useStableLoading(loading);

  // stableLoading stays true for 300ms after loading becomes false
  return <Spinner show={stableLoading} />;
}
Use this hook to prevent loading spinners from flickering when operations complete quickly.

useSynchronizingState

State that automatically synchronizes with a prop value.

Signature

function useSynchronizingState<V>(value: V): readonly [V, (v: V) => void]
value
V
required
Value to synchronize with

Usage

import useSynchronizingState from '@proton/hooks/useSynchronizingState';

function EditableField({ initialValue }: { initialValue: string }) {
  // State syncs with initialValue when it changes
  const [value, setValue] = useSynchronizingState(initialValue);

  return (
    <input 
      value={value} 
      onChange={(e) => setValue(e.target.value)} 
    />
  );
}
Unlike useState, this hook will update the state whenever the prop value changes (by pointer identity).

Build docs developers (and LLMs) love