Component properties

Components properties are passed as a parameter inside a functional component. In this example, the props are directly destructured inside the function parameter:

const UserCard = ({name, role, imageSrc}) => (
<div>
<img src={imageSrc} alt={name} />
<h3>{name}</h3>
<p>{role}</p>
</div>
);

Defining component props in TypeScript

You can either use a type or an interface to specify component props. An interface can be extended with further properties. This can be helpful for public APIs, while a type is more strict.

A typed version of the above UserCard component may look like this:

type UserCardProps = {
name: string;
role: string;
imageSrc: string;
};

const UserCard = ({name, role, imageSrc}: UserCardProps): JSX.Element => (
<div>
<img src={imageSrc} alt={name} />
<h3>{name}</h3>
<p>{role}</p>
</div>
);

TypeScript does static type checks at build time. Before TypeScript became a thing, there were PropTypes, which provided property type checks at runtime.

This is mostly not used anymore today and no longer bundled with React. It is still available as a separate NPM package.

Common Property types

You can pass anything as a property to a component. Besides primitive types and typed objects, you can also pass JSX, child elements, CSS styles or callback functions:

type MyComponentProps = {
// children is a special component property for container component
children ?: React.ReactNode;

// A single React element
childrenElement: JSX.Element;

// to pass through style props
style?: React.CSSProperties;

// form events! the generic parameter is the type of event.target
onChange?: React.FormEventHandler<HTMLInputElement>;
};

Parent-Child communication between components

You can also pass callback functions to child components, which can be used for parent-child communication between components. The example is a user dashboard, which iterates over all users of the application and displays them in an unordered list. For each user, it provides a "delete user" action. On click on the button, the user isn't deleted immediately but a confirmation dialog is shown first.

const UserDashboard = () => {
const [users, deleteUser] = useUsers();
const [pendingDelete, setPendingDelete] = useState<string|null>(null);

const onConfirm = async () => {
await deleteUser(pendingDelete);
};

const onClose = () => {
setPendingDelete(null);
}

return (
<>
{users ? (
<ul>
{users.map(user => (<li key={user.id}>
<UserCard {...user} />
<button onClick={() => setPendingDelete(user.id)}> delete user </button>
</li>))}
</ul>
) : (<div>Loading</div>)
}
{(pendingDelete !== null) && (<ConfirmDialog text={
<>Do you really want to delete user <strong>{user.name}</strong>?</>
}
onConfirm={onConfirm} onClose={onClose} />
)}
</>
)
};

The implementation of the confirmation dialog can look like this:

type ConfirmDialogProps = {
text: JSX.element;
onConfirm: () => Promise<void>;
onClose: () => void;
};

const ConfirmDialog = ({text, onConfirm, onClose}: ConfirmDialogProps): JSX.Element => {
const handleConfirm = async () => {
await onConfirm();
onClose();
}
return (
<dialog aria-modal="true">
<h2>Confirm</h2>
<p>{text}</p>
<button onClick={handleConfirm}> Confirm </button>
<button onClick={() => onClose()}> Cancel </button>
</dialog>
);
};