Skip to main content
The @proton/hooks package provides a collection of generic, reusable React hooks that solve common problems in React applications. These hooks are business-agnostic and don’t depend on Proton-specific logic.

Installation

yarn add @proton/hooks
This package has minimal dependencies and can be used in any React project.

Package Information

{
  "name": "@proton/hooks",
  "description": "Generic business use-case agnostic helper hooks"
}

Available Hooks

The package exports 13 carefully crafted hooks:

useLoading

Handle async operations with loading states

useStateRef

Combine state with ref for immediate access

useCombinedRefs

Merge multiple refs into a single ref

useInterval

Declarative interval with cleanup

Core Hooks

useLoading

Manage loading states for async operations with automatic cleanup:
import { useLoading } from '@proton/hooks';

function DataFetcher() {
  const [loading, withLoading] = useLoading();
  const [data, setData] = useState(null);
  
  const fetchData = async () => {
    await withLoading(async () => {
      const result = await api.getData();
      setData(result);
    });
  };
  
  return (
    <div>
      <button onClick={fetchData} disabled={loading}>
        {loading ? 'Loading...' : 'Fetch Data'}
      </button>
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  );
}
Auto-cleanup: The hook automatically handles component unmounting, preventing state updates on unmounted components.

Advanced Usage: Multiple Loading States

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

function MultiOperation() {
  const [loadingMap, withLoading] = useLoadingByKey();
  
  const operation1 = () => withLoading('op1', fetchData1());
  const operation2 = () => withLoading('op2', fetchData2());
  
  return (
    <>
      <button disabled={loadingMap.op1}>Operation 1</button>
      <button disabled={loadingMap.op2}>Operation 2</button>
    </>
  );
}

useStateRef

Combine useState and useRef to get both reactive updates and immediate access:
import { useStateRef } from '@proton/hooks';

function Counter() {
  const [count, setCount, countRef] = useStateRef(0);
  
  const handleClick = () => {
    setCount(count + 1);
    
    // countRef.current has the latest value immediately
    console.log('Current count:', countRef.current);
  };
  
  return <button onClick={handleClick}>Count: {count}</button>;
}
Use case: Perfect for event handlers that need immediate access to current state without waiting for re-render.

useCombinedRefs

Merge multiple refs into one, useful when using forwardRef with internal refs:
import { forwardRef, useRef } from 'react';
import { useCombinedRefs } from '@proton/hooks';

const Input = forwardRef<HTMLInputElement>((props, ref) => {
  const internalRef = useRef<HTMLInputElement>(null);
  const combinedRef = useCombinedRefs(ref, internalRef);
  
  // Use internalRef for internal logic
  const focus = () => internalRef.current?.focus();
  
  return <input ref={combinedRef} {...props} />;
});

useInterval

Declarative interval that cleans up automatically:
import { useInterval } from '@proton/hooks';

function Clock() {
  const [time, setTime] = useState(new Date());
  
  // Update every second
  useInterval(() => {
    setTime(new Date());
  }, 1000);
  
  return <div>{time.toLocaleTimeString()}</div>;
}
function PausableClock() {
  const [delay, setDelay] = useState(1000);
  
  useInterval(() => {
    console.log('Tick');
  }, delay);
  
  // Pause by setting delay to null
  const pause = () => setDelay(null);
  const resume = () => setDelay(1000);
}

State Management Hooks

usePrevious

Access the previous value of a state or prop:
import { usePrevious } from '@proton/hooks';

function Counter({ count }: { count: number }) {
  const previousCount = usePrevious(count);
  
  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {previousCount}</p>
    </div>
  );
}

usePreviousDistinct

Like usePrevious but only updates when the value actually changes:
import { usePreviousDistinct } from '@proton/hooks';

function UserProfile({ userId }: { userId: string }) {
  const previousUserId = usePreviousDistinct(userId);
  
  useEffect(() => {
    if (previousUserId && previousUserId !== userId) {
      // User changed, cleanup old data
    }
  }, [userId, previousUserId]);
}

useSynchronizingState

Synchronize local state with external props:
import { useSynchronizingState } from '@proton/hooks';

function ControlledInput({ value: externalValue }) {
  const [value, setValue] = useSynchronizingState(externalValue);
  
  // Automatically syncs when externalValue changes
  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  );
}

useControlled

Create controlled/uncontrolled components easily:
import { useControlled } from '@proton/hooks';

function Toggle({ value, defaultValue, onChange }) {
  const [isOn, setIsOn] = useControlled({
    controlled: value,
    default: defaultValue ?? false,
  });
  
  const handleToggle = () => {
    const newValue = !isOn;
    setIsOn(newValue);
    onChange?.(newValue);
  };
  
  return <button onClick={handleToggle}>{isOn ? 'ON' : 'OFF'}</button>;
}

// Use as controlled
<Toggle value={isOn} onChange={setIsOn} />

// Or uncontrolled
<Toggle defaultValue={false} />

Utility Hooks

useInstance

Create a stable instance that persists across renders:
import { useInstance } from '@proton/hooks';

function EventEmitter() {
  const emitter = useInstance(() => new EventEmitter());
  
  // emitter is created once and reused
  return <div>...</div>;
}
Initialization: The factory function runs only once. Changes to it won’t affect the instance.

useIsMounted

Check if component is still mounted:
import { useIsMounted } from '@proton/hooks';

function AsyncComponent() {
  const isMounted = useIsMounted();
  
  const fetchData = async () => {
    const data = await api.getData();
    
    if (isMounted()) {
      // Safe to update state
      setData(data);
    }
  };
}

useEffectOnce

Run an effect exactly once (like componentDidMount):
import { useEffectOnce } from '@proton/hooks';

function Analytics() {
  useEffectOnce(() => {
    trackPageView();
    
    return () => {
      // Cleanup on unmount
    };
  });
}

useAsyncError

Throw async errors to error boundaries:
import { useAsyncError } from '@proton/hooks';

function DataLoader() {
  const throwError = useAsyncError();
  
  const fetchData = async () => {
    try {
      await api.getData();
    } catch (error) {
      // Throw to nearest error boundary
      throwError(error);
    }
  };
}

Advanced Hooks

useStableLoading

Prevents loading state flicker for fast operations:
import { useStableLoading } from '@proton/hooks';

function QuickAction() {
  const [loading, withLoading] = useStableLoading();
  
  // Won't show loading for operations < 500ms
  const quickAction = () => withLoading(
    api.quickOperation()
  );
}

useDateCountdown

Countdown to a target date:
import { useDateCountdown } from '@proton/hooks';

function Countdown({ targetDate }: { targetDate: Date }) {
  const { days, hours, minutes, seconds, isExpired } = useDateCountdown(
    targetDate
  );
  
  if (isExpired) {
    return <div>Expired!</div>;
  }
  
  return (
    <div>
      {days}d {hours}h {minutes}m {seconds}s
    </div>
  );
}

useSearchParams

Work with URL search parameters:
import { useSearchParams } from '@proton/hooks';

function FilteredList() {
  const [params, setParams] = useSearchParams();
  
  const filter = params.get('filter') || 'all';
  
  const setFilter = (newFilter: string) => {
    setParams({ filter: newFilter });
  };
  
  return (
    <div>
      <button onClick={() => setFilter('active')}>Active</button>
      <button onClick={() => setFilter('archived')}>Archived</button>
    </div>
  );
}

Testing

All hooks are thoroughly tested:
# Run tests
yarn workspace @proton/hooks test

# Watch mode
yarn workspace @proton/hooks test:watch

Testing Example

import { renderHook, act } from '@testing-library/react';
import { useLoading } from '@proton/hooks';

test('useLoading manages loading state', async () => {
  const { result } = renderHook(() => useLoading());
  
  expect(result.current[0]).toBe(false);
  
  let promise;
  act(() => {
    promise = result.current[1](
      new Promise(resolve => setTimeout(resolve, 100))
    );
  });
  
  expect(result.current[0]).toBe(true);
  
  await act(() => promise);
  
  expect(result.current[0]).toBe(false);
});

TypeScript Support

All hooks are fully typed:
import type { WithLoading, LoadingByKey } from '@proton/hooks';

// Generic types available
const [loading, withLoading] = useLoading();
const result = await withLoading<UserData>(
  api.getUser()
);
// result is typed as UserData | void

Best Practices

Composition: These hooks are designed to be composed together:
function useDataFetcher() {
  const [loading, withLoading] = useLoading();
  const [data, setData, dataRef] = useStateRef(null);
  const isMounted = useIsMounted();
  
  const fetch = async () => {
    await withLoading(async () => {
      const result = await api.getData();
      if (isMounted()) {
        setData(result);
      }
    });
  };
  
  return { data, loading, fetch, dataRef };
}
Memory Leaks: Always use hooks like useLoading and useIsMounted to prevent state updates on unmounted components.
Custom Hooks: Build your own hooks by combining these primitives for application-specific logic.

Dependencies

Minimal dependencies for maximum portability:
{
  "react": "^18.3.1"
}
No external dependencies beyond React!

Build docs developers (and LLMs) love