Skip to main content

Common Component Props

WithLoading Types

Types for loading state management in components.
type WithLoading = <T>(
  promise: undefined | Promise<T | void> | (() => Promise<T | void>)
) => Promise<T | void>

type WithLoadingByKey = <T>(
  key: string,
  promise: undefined | Promise<T | void> | (() => Promise<T | void>)
) => Promise<T | void>

type LoadingByKey = { [key: string]: boolean }
import type { WithLoading } from '@proton/hooks';
import { useLoading } from '@proton/hooks';

interface ButtonProps {
  onClick: () => Promise<void>;
  children: React.ReactNode;
}

function AsyncButton({ onClick, children }: ButtonProps) {
  const [loading, withLoading] = useLoading();

  const handleClick = () => withLoading(onClick());

  return (
    <button onClick={handleClick} disabled={loading}>
      {loading ? 'Loading...' : children}
    </button>
  );
}

Ref Types

Combined Refs

Types for combining multiple React refs.
import type { Ref } from 'react';

type CombinedRef<T> = Ref<T>
import { forwardRef, useRef } from 'react';
import useCombinedRefs from '@proton/hooks/useCombinedRefs';

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

const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ value, onChange }, ref) => {
    const internalRef = useRef<HTMLInputElement>(null);
    const combinedRef = useCombinedRefs(ref, internalRef);

    return (
      <input
        ref={combinedRef}
        value={value}
        onChange={(e) => onChange(e.target.value)}
      />
    );
  }
);

Controlled Component Types

Controlled Props Pattern

Types for controlled/uncontrolled component patterns.
interface ControlledProps<T> {
  value?: T;
  defaultValue?: T;
  onChange?: (value: T) => void;
}
import useControlled from '@proton/hooks/useControlled';

interface TextInputProps extends ControlledProps<string> {
  placeholder?: string;
  disabled?: boolean;
}

function TextInput({
  value,
  defaultValue = '',
  onChange,
  placeholder,
  disabled
}: TextInputProps) {
  const [internalValue, setInternalValue] = useControlled(
    value,
    defaultValue
  );

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

  return (
    <input
      value={internalValue}
      onChange={(e) => handleChange(e.target.value)}
      placeholder={placeholder}
      disabled={disabled}
    />
  );
}

API Component Types

Query Function Type

Type for API query functions used with useApiResult.
type QueryFunction = (...args: any[]) => {
  method: string;
  url: string;
  data?: any;
  params?: any;
}
import type { QueryFunction } from '@proton/components';

const queryUser: QueryFunction = (userId: string) => ({
  method: 'get',
  url: `/users/${userId}`
});

const updateUser: QueryFunction = (userId: string, data: any) => ({
  method: 'put',
  url: `/users/${userId}`,
  data
});

API Result Type

Return type from useApiResult hook.
interface Result<R, U extends any[]> {
  result: R | undefined;
  error: Error;
  loading: boolean;
  request: (...args: U) => Promise<R>;
}
result
R | undefined
The fetched data (undefined while loading or on error)
error
Error
Error object if request failed
loading
boolean
Whether the request is in progress
request
(...args: U) => Promise<R>
Function to manually trigger the request
import type { Result } from '@proton/components';
import { useApiResult } from '@proton/components';

function DataComponent() {
  const apiResult: Result<User[], []> = useApiResult(
    queryUsers,
    []
  );

  const { result: users, loading, error, request } = apiResult;

  return (
    <div>
      {loading && <Spinner />}
      {error && <ErrorMessage error={error} />}
      {users?.map(user => <UserCard key={user.ID} user={user} />)}
      <button onClick={() => request()}>Refresh</button>
    </div>
  );
}

Cache Types

Cache Interface

Interface for the application cache.
interface Cache<K = string, V = any> {
  get(key: K): V | undefined;
  set(key: K, value: V): void;
  has(key: K): boolean;
  delete(key: K): boolean;
  clear(): void;
  subscribe(callback: (key: K, value: V) => void): () => void;
}
import { useCache } from '@proton/components';
import type { Cache } from '@proton/shared/lib/helpers/cache';

interface User {
  id: string;
  name: string;
}

function UserManager() {
  const cache: Cache<string, User> = useCache();

  const saveUser = (user: User) => {
    cache.set(`user-${user.id}`, user);
  };

  const getUser = (id: string): User | undefined => {
    return cache.get(`user-${id}`);
  };

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

Event Handler Types

Common Event Handlers

Typescript types for common React event handlers.
import type { MouseEvent, ChangeEvent, FormEvent } from 'react';

type ClickHandler = (event: MouseEvent<HTMLElement>) => void;
type ChangeHandler = (event: ChangeEvent<HTMLInputElement>) => void;
type SubmitHandler = (event: FormEvent<HTMLFormElement>) => void;
interface ButtonProps {
  onClick: ClickHandler;
  children: React.ReactNode;
}

function Button({ onClick, children }: ButtonProps) {
  return <button onClick={onClick}>{children}</button>;
}

Polymorphic Component Types

Types for components that can render as different HTML elements.
import type { ComponentPropsWithoutRef, ElementType } from 'react';

type PolymorphicProps<E extends ElementType> = {
  as?: E;
} & ComponentPropsWithoutRef<E>;
import type { ElementType } from 'react';

interface BoxProps<E extends ElementType = 'div'> {
  as?: E;
  children: React.ReactNode;
}

function Box<E extends ElementType = 'div'>({
  as,
  children,
  ...props
}: BoxProps<E> & Omit<ComponentPropsWithoutRef<E>, keyof BoxProps<E>>) {
  const Component = as || 'div';
  return <Component {...props}>{children}</Component>;
}

// Usage
<Box as="section">Content</Box>
<Box as="article">Article</Box>
<Box>Div by default</Box>

Best Practices

Use Generic Types

// Good - reusable with any type
function DataList<T>({ items }: { items: T[] }) {
  return <ul>{items.map((item, i) => <li key={i}>{item}</li>)}</ul>;
}

// Usage
<DataList<User> items={users} />
<DataList<Product> items={products} />

Type Props Explicitly

// Good
interface UserCardProps {
  user: User;
  onEdit: (user: User) => void;
  loading?: boolean;
}

function UserCard({ user, onEdit, loading = false }: UserCardProps) {
  // ...
}

// Avoid
function UserCard(props: any) {
  // ...
}

Use Discriminated Unions for Variants

type ButtonProps =
  | { variant: 'link'; href: string; onClick?: never }
  | { variant: 'button'; onClick: () => void; href?: never };

function Button(props: ButtonProps) {
  if (props.variant === 'link') {
    return <a href={props.href}>Link</a>;
  }
  return <button onClick={props.onClick}>Button</button>;
}

Build docs developers (and LLMs) love