• Что бы вступить в ряды "Принятый кодер" Вам нужно:
    Написать 10 полезных сообщений или тем и Получить 10 симпатий.
    Для того кто не хочет терять время,может пожертвовать средства для поддержки сервеса, и вступить в ряды VIP на месяц, дополнительная информация в лс.

  • Пользаватели которые будут спамить, уходят в бан без предупреждения. Спам сообщения определяется администрацией и модератором.

  • Гость, Что бы Вы хотели увидеть на нашем Форуме? Изложить свои идеи и пожелания по улучшению форума Вы можете поделиться с нами здесь. ----> Перейдите сюда
  • Все пользователи не прошедшие проверку электронной почты будут заблокированы. Все вопросы с разблокировкой обращайтесь по адресу электронной почте : info@guardianelinks.com . Не пришло сообщение о проверке или о сбросе также сообщите нам.

TypeScript Generics in React Components: A Complete Guide

Sascha Оффлайн

Sascha

Заместитель Администратора
Команда форума
Администратор
Регистрация
9 Май 2015
Сообщения
1,483
Баллы
155


I didn't want to write about using Generics in React Components until last week, when I received two comments from the engineers on my team.

The syntax looks invalid
The usage of generics throws me off
At that moment, I realised that this was an opportunity to share what I knew about using TypeScript Generics in React Components that could help some engineers, and get the team aligned on why, how, and when to use generics.

1. Introduction: Why Generics in React Components?


When building React apps with TypeScript, you'll often find yourself creating components that work with different types of data but follow the same structure. Without generics, you might end up with repetitive code or lose type safety.

Consider this common scenario: you have a list component that displays users in one place and products in another. Without generics, you might create separate components or use any types, both of which lead to problems.

The problem with non-generic components:


// Without generics - not reusable
interface UserListProps {
items: User[];
onSelect: (item: User) => void;
}

interface ProductListProps {
items: Product[];
onSelect: (item: Product) => void;
}

// Or worse - lose type safety
interface GenericListProps {
items: any[];
onSelect: (item: any) => void;
}




Type safety benefits of generics include catching errors at compile time, better IntelliSense support, and self-documenting code. Code reusability and maintainability improve because you write the logic once and apply it to multiple types.

2. TypeScript Generics Fundamentals


Before diving into React-specific patterns, let's review the basics of

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

.

Generic syntax uses angle brackets to define type parameters:


function echo<T>(arg: T): T {
return arg;
}

// Usage
const result = echo<string>("hello"); // T is string
const number = echo(42); // T is inferred as number




Generic constraints and bounds limit what types can be used:


interface HasLength {
length: number;
}

function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length);
return arg;
}

logLength("hello"); // Works - string has length
logLength([1, 2, 3]); // Works - array has length
// logLength(42); // Error - number doesn't have length




Default generic parameters provide fallback types:


interface ApiResponse<TData = unknown> {
data: TData;
status: number;
}

// Uses default unknown type
const response: ApiResponse = { data: {}, status: 200 };

// Explicit type
const userResponse: ApiResponse<User> = { data: user, status: 200 };



3. Your First Generic React Component


Let's convert a simple component to use generics. Here's a basic list component:


// Non-generic version
interface User {
id: number;
name: string;
email: string;
}

interface UserListProps {
users: User[];
onUserSelect: (user: User) => void;
}

function UserList({ users, onUserSelect }: UserListProps) {
return (
<ul>
{users.map(user => (
<li key={user.id} onClick={() => onUserSelect(user)}>
{user.name}
</li>
))}
</ul>
);
}




Now let's make it generic:


// Generic version
interface ListItem {
id: number | string;
}

interface GenericListProps<T extends ListItem> {
items: T[];
onItemSelect: (item: T) => void;
renderItem: (item: T) => React.ReactNode;
}

function GenericList<T extends ListItem>({
items,
onItemSelect,
renderItem
}: GenericListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onItemSelect(item)}>
{renderItem(item)}
</li>
))}
</ul>
);
}




Use generics:


// Usage with explicit type
<GenericList<User>
items={users}
onItemSelect={handleUserSelect}
renderItem={(user) => user.name}
/>

// TypeScript can often infer the type
<GenericList
items={products} // TypeScript infers Product[]
onItemSelect={handleProductSelect}
renderItem={(product) => product.title}
/>




Props interface with generic types ensures type safety throughout the component. The T extends ListItem constraint ensures all items have an id property for the key prop.

Type safety? Yes.


Imagine you pass the wrong handler to the list.


<GenericList
items={products} // Type 'Product[]' is not assignable to type 'User[]'.
onItemSelect={handleUserSelect}
renderItem={(product) => product.title}
/>




TypeScript will pick it up before shipping this bug to your pipeline or your customers.

An example can be found

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

.

4. Common Patterns and Use Cases


Generic list components are perfect for displaying collections of any type:


interface TableColumn<T> {
key: keyof T;
title: string;
render?: (value: T[keyof T], item: T) => React.ReactNode;
}

interface DataTableProps<T> {
data: T[];
columns: TableColumn<T>[];
onRowClick?: (item: T) => void;
}

function DataTable<T>({ data, columns, onRowClick }: DataTableProps<T>) {
return (
<table>
<thead>
<tr>
{columns.map(col => (
<th key={String(col.key)}>{col.title}</th>
))}
</tr>
</thead>
<tbody>
{data.map((item, index) => (
<tr key={index} onClick={() => onRowClick?.(item)}>
{columns.map(col => (
<td key={String(col.key)}>
{col.render
? col.render(item[col.key], item)
: String(item[col.key])
}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}




Form components with typed values:


interface FormFieldProps<T> {
value: T;
onChange: (value: T) => void;
label: string;
error?: string;
}

function FormField<T extends string | number>({
value,
onChange,
label,
error
}: FormFieldProps<T>) {
return (
<div>
<label>{label}</label>
<input
type={typeof value === 'number' ? 'number' : 'text'}
value={value}
onChange={(e) => {
const newValue = typeof value === 'number'
? Number(e.target.value) as T
: e.target.value as T;
onChange(newValue);
}}
/>
{error && <span className="error">{error}</span>}
</div>
);
}




Modal components with typed data:


interface ModalProps<T> {
isOpen: boolean;
onClose: () => void;
data: T;
renderContent: (data: T) => React.ReactNode;
title?: string;
}

function Modal<T>({
isOpen,
onClose,
data,
renderContent,
title
}: ModalProps<T>) {
if (!isOpen) return null;

return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{title && <h2>{title}</h2>}
<button onClick={onClose}>×</button>
{renderContent(data)}
</div>
</div>
);
}

// Usage
<Modal<User>
isOpen={showUserModal}
onClose={() => setShowUserModal(false)}
data={selectedUser}
title="User Details"
renderContent={(user) => (
<div>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</div>
)}
/>



5. Prop Inference and Type Safety


TypeScript's type inference makes generic components feel natural to use. Here's how it works:

How TypeScript infers generic types from props:


interface SelectProps<T> {
options: T[];
value: T;
onChange: (value: T) => void;
getLabel: (option: T) => string;
getValue: (option: T) => string | number;
}

function Select<T>(props: SelectProps<T>) {
// Component implementation
}

// TypeScript infers T as User from the options prop
<Select
options={users} // User[]
value={selectedUser} // User
onChange={setSelectedUser} // (user: User) => void
getLabel={(user) => user.name} // user is typed as User
getValue={(user) => user.id} // user is typed as User
/>




Explicit vs implicit type parameters:


// Explicit - useful when inference isn't possible
<Select<User>
options={[]}
value={undefined}
onChange={handleChange}
getLabel={(user) => user.name}
getValue={(user) => user.id}
/>

// Implicit - TypeScript figures it out
<Select
options={users}
value={selectedUser}
onChange={handleChange}
getLabel={(user) => user.name}
getValue={(user) => user.id}
/>




Type narrowing in component props happens when you provide more specific constraints:


interface SearchableItem {
searchableText: string;
}

interface SearchableListProps<T extends SearchableItem> {
items: T[];
onSearch: (query: string, items: T[]) => T[];
}

function SearchableList<T extends SearchableItem>(props: SearchableListProps<T>) {
// T is guaranteed to have searchableText property
const filteredItems = props.items.filter(item =>
item.searchableText.toLowerCase().includes(searchQuery.toLowerCase())
);
}



6. Advanced Generic Patterns


Multiple generic parameters allow for complex component relationships:


interface KeyValuePair<K, V> {
key: K;
value: V;
}

interface KeyValueListProps<K, V> {
pairs: KeyValuePair<K, V>[];
onPairSelect: (key: K, value: V) => void;
renderKey: (key: K) => React.ReactNode;
renderValue: (value: V) => React.ReactNode;
}

function KeyValueList<K, V>({
pairs,
onPairSelect,
renderKey,
renderValue
}: KeyValueListProps<K, V>) {
return (
<div>
{pairs.map((pair, index) => (
<div
key={index}
onClick={() => onPairSelect(pair.key, pair.value)}
>
<span>{renderKey(pair.key)}</span>:
<span>{renderValue(pair.value)}</span>
</div>
))}
</div>
);
}




Generic constraints with extends provide powerful type checking:


interface Timestamped {
createdAt: Date;
updatedAt: Date;
}

interface TimelineProps<T extends Timestamped> {
items: T[];
renderItem: (item: T) => React.ReactNode;
sortOrder: 'asc' | 'desc';
}

function Timeline<T extends Timestamped>({
items,
renderItem,
sortOrder
}: TimelineProps<T>) {
const sortedItems = [...items].sort((a, b) => {
const dateA = a.createdAt.getTime();
const dateB = b.createdAt.getTime();
return sortOrder === 'asc' ? dateA - dateB : dateB - dateA;
});

return (
<div className="timeline">
{sortedItems.map((item, index) => (
<div key={index} className="timeline-item">
<time>{item.createdAt.toLocaleDateString()}</time>
{renderItem(item)}
</div>
))}
</div>
);
}




Conditional types in component props:


type ConditionalProps<T> = T extends string
? { stringProp: string }
: T extends number
? { numberProp: number }
: { genericProp: T };

interface ConditionalComponentProps<T> extends ConditionalProps<T> {
value: T;
}

function ConditionalComponent<T>(props: ConditionalComponentProps<T>) {
// Component logic based on conditional props
}




Generic utility types (Pick, Omit, Partial):


interface FullUser {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}

interface UserCardProps<T extends Pick<FullUser, 'id' | 'name' | 'email'>> {
user: T;
onEdit?: (user: T) => void;
showEmail?: boolean;
}

function UserCard<T extends Pick<FullUser, 'id' | 'name' | 'email'>>({
user,
onEdit,
showEmail = true
}: UserCardProps<T>) {
return (
<div className="user-card">
<h3>{user.name}</h3>
{showEmail && <p>{user.email}</p>}
{onEdit && <button onClick={() => onEdit(user)}>Edit</button>}
</div>
);
}



7. Generic Hooks and Custom Logic


Creating generic custom hooks:


function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});

const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};

return [storedValue, setValue] as const;
}

// Usage in component
function UserPreferences() {
const [preferences, setPreferences] = useLocalStorage<UserPrefs>('userPrefs', {
theme: 'light',
language: 'en'
});
}




Combining generic components with generic hooks:


function useAsyncData<T>(fetchFn: () => Promise<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
setLoading(true);
fetchFn()
.then(setData)
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, []);

return { data, loading, error };
}

interface AsyncDataDisplayProps<T> {
fetchFn: () => Promise<T>;
renderData: (data: T) => React.ReactNode;
renderLoading?: () => React.ReactNode;
renderError?: (error: string) => React.ReactNode;
}

function AsyncDataDisplay<T>({
fetchFn,
renderData,
renderLoading = () => <div>Loading...</div>,
renderError = (error) => <div>Error: {error}</div>
}: AsyncDataDisplayProps<T>) {
const { data, loading, error } = useAsyncData(fetchFn);

if (loading) return renderLoading();
if (error) return renderError(error);
if (!data) return null;

return <>{renderData(data)}</>;
}



8. Real-World Examples


Building a generic data fetcher component:


interface ApiConfig {
baseURL: string;
headers?: Record<string, string>;
}

interface DataFetcherProps<T> {
url: string;
config?: ApiConfig;
transform?: (data: any) => T;
renderData: (data: T) => React.ReactNode;
renderLoading?: () => React.ReactNode;
renderError?: (error: Error) => React.ReactNode;
}

function DataFetcher<T>({
url,
config = { baseURL: '' },
transform = (data) => data,
renderData,
renderLoading = () => <div>Loading...</div>,
renderError = (error) => <div>Error: {error.message}</div>
}: DataFetcherProps<T>) {
const [state, setState] = useState<{
data: T | null;
loading: boolean;
error: Error | null;
}>({
data: null,
loading: true,
error: null
});

useEffect(() => {
const fetchData = async () => {
try {
setState(prev => ({ ...prev, loading: true, error: null }));
const response = await fetch(`${config.baseURL}${url}`, {
headers: config.headers
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const rawData = await response.json();
const transformedData = transform(rawData);
setState({ data: transformedData, loading: false, error: null });
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error')
});
}
};

fetchData();
}, [url, config.baseURL]);

if (state.loading) return renderLoading();
if (state.error) return renderError(state.error);
if (!state.data) return null;

return <>{renderData(state.data)}</>;
}

// Usage
interface User {
id: number;
name: string;
email: string;
}

<DataFetcher<User[]>
url="/api/users"
config={{ baseURL: '

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

' }}
transform={(data) => data.users}
renderData={(users) => (
<ul>
{users.map(user => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
)}
/>




Creating a reusable form field component:


interface ValidationRule<T> {
validate: (value: T) => boolean;
message: string;
}

interface FormFieldProps<T> {
label: string;
value: T;
onChange: (value: T) => void;
type?: 'text' | 'email' | 'number' | 'password';
placeholder?: string;
required?: boolean;
validationRules?: ValidationRule<T>[];
renderInput?: (props: {
value: T;
onChange: (value: T) => void;
hasError: boolean;
}) => React.ReactNode;
}

function FormField<T extends string | number>({
label,
value,
onChange,
type = 'text',
placeholder,
required = false,
validationRules = [],
renderInput
}: FormFieldProps<T>) {
const [touched, setTouched] = useState(false);

const errors = validationRules
.filter(rule => !rule.validate(value))
.map(rule => rule.message);

const hasError = touched && errors.length > 0;

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = type === 'number'
? Number(e.target.value) as T
: e.target.value as T;
onChange(newValue);
};

const defaultInput = (
<input
type={type}
value={value}
onChange={handleChange}
onBlur={() => setTouched(true)}
placeholder={placeholder}
className={hasError ? 'error' : ''}
required={required}
/>
);

return (
<div className="form-field">
<label>
{label}
{required && <span className="required">*</span>}
</label>
{renderInput
? renderInput({ value, onChange, hasError })
: defaultInput
}
{hasError && (
<div className="error-messages">
{errors.map((error, index) => (
<span key={index} className="error-message">
{error}
</span>
))}
</div>
)}
</div>
);
}

// Usage
const emailValidation: ValidationRule<string> = {
validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
message: 'Please enter a valid email address'
};

<FormField<string>
label="Email"
value={email}
onChange={setEmail}
type="email"
required
validationRules={[emailValidation]}
/>



9. Best Practices and Common Pitfalls


When to use generics vs unions:

Use generics when you need the same structure with different types:


// Good use of generics
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}




Use unions when you have a limited set of known types:


// Good use of unions
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size: 'small' | 'medium' | 'large';
}




Naming conventions for generic parameters:

  • Use T for a single generic type
  • Use descriptive names for multiple parameters: TData, TError, TKey, TValue
  • Use T prefix for generic type parameters to distinguish from regular types

// Good naming
interface DataTableProps<TData, TColumn> {
data: TData[];
columns: TColumn[];
}

// Clear and descriptive
interface ApiHookResult<TData, TError = Error> {
data: TData | null;
error: TError | null;
loading: boolean;
}




Performance considerations:

  • Generic components don't have runtime overhead
  • TypeScript generics are compile-time only
  • Be mindful of complex conditional types that can slow down compilation

Debugging generic type errors:


// Use type assertions for debugging
const debugType = <T>(value: T): T => {
console.log('Type:', typeof value, 'Value:', value);
return value;
};

// Use utility types to inspect complex types
type InspectProps<T> = {
[K in keyof T]: T[K]
};



10. Integration with Popular Libraries


Generics with React Query/SWR:


import { useQuery } from '@tanstack/react-query';

interface QueryComponentProps<TData, TError = Error> {
queryKey: string[];
queryFn: () => Promise<TData>;
renderData: (data: TData) => React.ReactNode;
renderError?: (error: TError) => React.ReactNode;
}

function QueryComponent<TData, TError = Error>({
queryKey,
queryFn,
renderData,
renderError = (error) => <div>Error: {String(error)}</div>
}: QueryComponentProps<TData, TError>) {
const { data, error, isLoading } = useQuery<TData, TError>({
queryKey,
queryFn
});

if (isLoading) return <div>Loading...</div>;
if (error) return renderError(error);
if (!data) return null;

return <>{renderData(data)}</>;
}




Form libraries (React Hook Form):


import { useForm, FieldValues, Path } from 'react-hook-form';

interface FormInputProps<T extends FieldValues> {
name: Path<T>;
label: string;
register: ReturnType<typeof useForm<T>>['register'];
errors: ReturnType<typeof useForm<T>>['formState']['errors'];
type?: string;
}

function FormInput<T extends FieldValues>({
name,
label,
register,
errors,
type = 'text'
}: FormInputProps<T>) {
const error = errors[name];

return (
<div>
<label htmlFor={name}>{label}</label>
<input
id={name}
type={type}
{...register(name)}
className={error ? 'error' : ''}
/>
{error && <span className="error">{error.message}</span>}
</div>
);
}



11. Testing Generic Components


Writing tests for generic components:


import { render, screen, fireEvent } from '@testing-library/react';
import { GenericList } from './GenericList';

interface TestItem {
id: number;
name: string;
}

describe('GenericList', () => {
const mockItems: TestItem[] = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
];

it('renders items correctly', () => {
const mockOnSelect = jest.fn();

render(
<GenericList<TestItem>
items={mockItems}
onItemSelect={mockOnSelect}
renderItem={(item) => item.name}
/>
);

expect(screen.getByText('Item 1')).toBeInTheDocument();
expect(screen.getByText('Item 2')).toBeInTheDocument();
});

it('calls onItemSelect with correct type', () => {
const mockOnSelect = jest.fn();

render(
<GenericList<TestItem>
items={mockItems}
onItemSelect={mockOnSelect}
renderItem={(item) => item.name}
/>
);

fireEvent.click(screen.getByText('Item 1'));

expect(mockOnSelect).toHaveBeenCalledWith(mockItems[0]);
// TypeScript ensures the parameter is TestItem, not any
});
});




Mocking generic props:


// Create typed mocks for better test safety
const createMockProps = <T>(): GenericListProps<T> => ({
items: [] as T[],
onItemSelect: jest.fn(),
renderItem: jest.fn().mockReturnValue('mocked content')
});

// Use in tests
const mockProps = createMockProps<TestItem>();




Type assertion strategies:


// When you need to assert types in tests
const assertType = <T>(value: unknown): asserts value is T => {
// Runtime type checking logic if needed
};

// Or use TypeScript's assertion functions
function isTestItem(item: unknown): item is TestItem {
return typeof item === 'object' &&
item !== null &&
'id' in item &&
'name' in item;
}



Conclusion


TypeScript generics in React components provide a powerful way to create reusable, type-safe components that work with different data types while maintaining excellent developer experience. By following the patterns and best practices outlined in this guide, your team can build more maintainable and robust React applications.

The key is to start simple with basic generic components and gradually adopt more advanced patterns as your needs grow. Remember that generics should make your code more reusable and type-safe, not more complex. When in doubt, prefer explicit types over overly complex generic constraints. (Yes, I've seen too many over-engineering examples, including all of mine from the past decades)

Start implementing these patterns in your codebase today, and you'll find that your components become more flexible, your code becomes more maintainable, and your development experience improves significantly.

References and Further Reading

Official Documentation

In-Depth Articles and Tutorials

Practical Guides and Tips

Community Resources



These references provide a mix of official documentation, practical tutorials, and community resources that complement the topics covered in this guide. They range from beginner-friendly explanations to advanced patterns, giving your team multiple perspectives and learning paths for mastering TypeScript generics in React.



Источник:

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

 
Вверх Снизу