In React, dealing with props might be too verbose and not efficient. An alternative is to use the React Context API. It is useful for sharing data between the app.
A practical example:
- Add an employee
- View a list of employees
Define the properties and types of an employee
// file: types/index.ts
export type EmployeeType = {
name: string;
age: number;
email: string;
dateOfBirth: Date;
};
export type EmployeesType = EmployeeType[];
Define the context and the provider.
- We are defining a kind of contract : every components in the tree can access the context. Here, there are an array of employees and a function to add an employee to this array.
- The provider implements the context using the
useState
hook to store employees. - We add a custom Hook to force a good usage of the provider.
// file: context/EmployeesContext.ts
import { PropsWithChildren, createContext, useContext, useState } from 'react';
import { EmployeeType, EmployeesType } from '../types';
// Contract or interface for the context ~API
type EmployeesContextProps = {
employees: EmployeesType;
addEmployee: (newEmployee: EmployeeType) => void;
};
// Create the Context :
// passing data down the component tree, making it accessible
const EmployeesContext = createContext<EmployeesContextProps | null>(null);
// Create the Provider
export const EmployeesProvider = ({ children }: PropsWithChildren) => {
// useState: manage and share stateful data in App
const [employees, setEmployees] = useState<EmployeesType>([]);
// addEmployee implementation : add to the existing array
const addEmployee = (newEmployee: EmployeeType) => {
setEmployees([...employees, newEmployee]);
};
// Render
return (
<EmployeesContext.Provider value={{ employees, addEmployee }}>
{children}
</EmployeesContext.Provider>
);
};
// Custom hook ensuring that it is used in the appropriate context provider
export const useEmployeesContext = () => {
const context = useContext(EmployeesContext);
if (!context) {
throw new Error('useEmployeesContext must be used inside the EmployeesProvider');
}
return context;
};
Insert components inside the EmployeesProvider
// file: App.tsx
import EmployeesList from './components/EmployeesList';
import EmployeeForm from './components/EmployeeForm';
import { EmployeesProvider } from './context/EmployeesContext';
const App = () => {
return (
<EmployeesProvider>
<EmployeeForm />
<EmployeesList />
</EmployeesProvider>
);
};
export default App;
Display the employees in a simple list. We use our custom hook to access the "employees" array.
// file: components/List.tsx
import { useEmployeesContext } from '../context/EmployeesContext';
const EmployeesList = () => {
const { employees } = useEmployeesContext();
return (
<ul>
{employees.map((employee, index) => {
return (
<li key={index}>
{employee.name} - {employee.age} years - {employee.email} -
{employee.dateOfBirth.toLocaleString()}
</li>
);
})}
</ul>
);
};
export default EmployeesList;
The form use the custom hook to access the "addEmployee" function. We combine all input values in a FormData structure in respect with the "EmployeeType" provided by the context.
// file: components/Form.tsx
import { useEmployeesContext } from '../context/EmployeesContext';
const EmployeeForm = () => {
const { addEmployee } = useEmployeesContext();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const formData = new FormData(form);
const newEmployee = {
name: formData.get('name') as string,
age: parseInt(formData.get('age') as string),
email: formData.get('email') as string,
dateOfBirth: new Date(formData.get('dateOfBirth') as string),
};
addEmployee(newEmployee); // the context is typed, no need to import type
form.reset();
};
return (
<form onSubmit={handleSubmit}>
<label>Name<input type="text" name="name" /></label>
<label>Age<input type="number" name="age" /></label>
<label>Email<input type="email" name="email" /></label>
<label>Date of Birth<input type="date" name="dateOfBirth" /></label>
<button type="submit">Add Employee</button>
</form>
);
};
export default EmployeeForm;