Frontend Development Patterns - React & Type-
Script
Table of Contents
1. React Component Patterns
2. State Management
3. TypeScript in React
4. Performance Optimization
5. Testing Strategies
6. CSS and Styling
7. Form Handling
8. API Integration
9. Routing and Navigation
10. Build and Deployment
1. React Component Patterns
Functional Components
Modern React uses functional components exclusively. They’re simpler than
class components and work with hooks. Components are pure functions that
accept props and return JSX.
interface UserCardProps {
name: string;
email: string;
avatar?: string;
}
export const UserCard: [Link]<UserCardProps> = ({ name, email, avatar }) => {
return (
<div className="user-card">
{avatar && <img src={avatar} alt={name} />}
<h3>{name}</h3>
<p>{email}</p>
</div>
);
};
Component Composition
Complex UIs are built through composition: combining simple components into
complex ones. This creates reusable building blocks.
1
export const UserList: [Link]<{ users: User[] }> = ({ users }) => {
return (
<div className="user-list">
{[Link](user => (
<UserCard key={[Link]} {...user} />
))}
</div>
);
};
Container and Presentational Components
Containers handle logic and data fetching. Presentational components receive
data through props and focus on rendering. This separation improves testability
and reusability.
// Presentational
const UserListView: [Link]<{ users: User[]; loading: boolean }> = ({
users,
loading
}) => {
if (loading) return <Spinner />;
return <UserList users={users} />;
};
// Container
const UserListContainer: [Link] = () => {
const { data: users, isLoading } = useQuery('users', fetchUsers);
return <UserListView users={users ?? []} loading={isLoading} />;
};
Higher-Order Components
Higher-Order Components wrap components to add functionality. They take a
component and return an enhanced component.
function withAuth<P extends object>(
Component: [Link]<P>
): [Link]<P> {
return (props) => {
const { user, isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Redirect to="/login" />;
}
return <Component {...props} user={user} />;
2
};
}
const ProtectedPage = withAuth(Dashboard);
Render Props Pattern
Components with render props accept functions as children. These functions
receive data and return UI.
interface DataFetcherProps<T> {
url: string;
children: (data: T, loading: boolean, error: Error | null) => ReactNode;
}
function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then(res => [Link]())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [url]);
return <>{children(data, loading, error)}</>;
}
// Usage
<DataFetcher<User[]> url="/api/users">
{(users, loading, error) => {
if (loading) return <Spinner />;
if (error) return <Error message={[Link]} />;
return <UserList users={users} />;
}}
</DataFetcher>
3
Custom Hooks
Custom hooks extract component logic into reusable functions. They can use
other hooks and must follow hook naming conventions (start with “use”).
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = [Link](key);
return stored ? [Link](stored) : initialValue;
});
useEffect(() => {
[Link](key, [Link](value));
}, [key, value]);
return [value, setValue] as const;
}
// Usage
const [theme, setTheme] = useLocalStorage('theme', 'light');
Compound Components
Compound components work together to form a complete UI pattern. They
share implicit state.
const TabsContext = createContext<{
activeTab: string;
setActiveTab: (tab: string) => void;
} | null>(null);
const Tabs: [Link]<{ children: ReactNode; defaultTab: string }> = ({
children,
defaultTab
}) => {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<[Link] value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</[Link]>
);
};
const TabList: [Link]<{ children: ReactNode }> = ({ children }) => {
return <div className="tab-list">{children}</div>;
};
4
const Tab: [Link]<{ value: string; children: ReactNode }> = ({
value,
children
}) => {
const context = useContext(TabsContext);
if (!context) throw new Error('Tab must be used within Tabs');
return (
<button
className={[Link] === value ? 'active' : ''}
onClick={() => [Link](value)}
>
{children}
</button>
);
};
const TabPanel: [Link]<{ value: string; children: ReactNode }> = ({
value,
children
}) => {
const context = useContext(TabsContext);
if (!context) throw new Error('TabPanel must be used within Tabs');
return [Link] === value ? <div>{children}</div> : null;
};
// Usage
<Tabs defaultTab="profile">
<TabList>
<Tab value="profile">Profile</Tab>
<Tab value="settings">Settings</Tab>
</TabList>
<TabPanel value="profile">Profile content</TabPanel>
<TabPanel value="settings">Settings content</TabPanel>
</Tabs>
2. State Management
useState Hook
Local component state uses the useState hook. State updates trigger re-renders.
5
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
// Functional updates for state depending on previous value
setCount(prev => prev + 1);
useReducer Hook
Complex state logic uses useReducer. It’s similar to Redux but local to compo-
nent.
interface State {
count: number;
step: number;
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'setStep'; payload: number };
function reducer(state: State, action: Action): State {
switch ([Link]) {
case 'increment':
return { ...state, count: [Link] + [Link] };
case 'decrement':
return { ...state, count: [Link] - [Link] };
case 'setStep':
return { ...state, step: [Link] };
default:
return state;
}
}
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
dispatch({ type: 'increment' });
dispatch({ type: 'setStep', payload: 5 });
Context API
Context shares data across component tree without prop drilling. Used for
themes, authentication, localization.
interface AuthContext {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
6
}
const AuthContext = createContext<AuthContext | null>(null);
export const AuthProvider: [Link]<{ children: ReactNode }> = ({
children
}) => {
const [user, setUser] = useState<User | null>(null);
const login = async (email: string, password: string) => {
const user = await [Link](email, password);
setUser(user);
};
const logout = () => {
setUser(null);
};
return (
<[Link] value={{ user, login, logout }}>
{children}
</[Link]>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within AuthProvider');
return context;
};
Zustand
Zustand provides simple, unopinionated state management. It’s lighter than
Redux with less boilerplate.
import create from 'zustand';
interface BearStore {
bears: number;
increase: () => void;
decrease: () => void;
}
const useBearStore = create<BearStore>((set) => ({
bears: 0,
7
increase: () => set((state) => ({ bears: [Link] + 1 })),
decrease: () => set((state) => ({ bears: [Link] - 1 })),
}));
// Usage
const bears = useBearStore((state) => [Link]);
const increase = useBearStore((state) => [Link]);
Redux Toolkit
Redux Toolkit simplifies Redux with less boilerplate. It includes createSlice for
reducers and actions, createAsyncThunk for async logic.
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
interface UserState {
user: User | null;
loading: boolean;
error: string | null;
}
export const fetchUser = createAsyncThunk(
'user/fetch',
async (userId: string) => {
const response = await [Link](userId);
return [Link];
}
);
const userSlice = createSlice({
name: 'user',
initialState: { user: null, loading: false, error: null } as UserState,
reducers: {
logout: (state) => {
[Link] = null;
},
},
extraReducers: (builder) => {
builder
.addCase([Link], (state) => {
[Link] = true;
})
.addCase([Link], (state, action) => {
[Link] = false;
[Link] = [Link];
})
8
.addCase([Link], (state, action) => {
[Link] = false;
[Link] = [Link] ?? 'Failed to fetch user';
});
},
});
export const { logout } = [Link];
export default [Link];
React Query
React Query manages server state: fetching, caching, synchronizing. It elimi-
nates manual state management for async data.
import { useQuery, useMutation, useQueryClient } from 'react-query';
function useUsers() {
return useQuery('users', fetchUsers, {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
});
}
function useCreateUser() {
const queryClient = useQueryClient();
return useMutation(createUser, {
onSuccess: () => {
[Link]('users');
},
});
}
// Usage
const { data: users, isLoading, error } = useUsers();
const createUserMutation = useCreateUser();
const handleCreate = () => {
[Link]({ name: 'Alice', email: 'alice@[Link]' });
};
9
3. TypeScript in React
Component Props Types
Props are typed with interfaces or type aliases. Props are documented through
types.
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
onClick: () => void;
children: ReactNode;
}
const Button: [Link]<ButtonProps> = ({
variant,
size = 'medium',
disabled = false,
onClick,
children
}) => {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
};
Event Types
Event handlers are typed with React event types.
const handleChange = (e: [Link]<HTMLInputElement>) => {
setValue([Link]);
};
const handleSubmit = (e: [Link]<HTMLFormElement>) => {
[Link]();
// Handle form submission
};
const handleClick = (e: [Link]<HTMLButtonElement>) => {
[Link]([Link]);
10
};
Generic Components
Generic components work with multiple types while maintaining type safety.
interface ListProps<T> {
items: T[];
renderItem: (item: T) => ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{[Link](item => (
<li key={keyExtractor(item)}>
{renderItem(item)}
</li>
))}
</ul>
);
}
// Usage
<List
items={users}
renderItem={(user) => <UserCard {...user} />}
keyExtractor={(user) => [Link]}
/>
Utility Types
TypeScript utility types simplify type definitions.
// Pick specific properties
type UserPreview = Pick<User, 'id' | 'name' | 'avatar'>;
// Omit specific properties
type UserWithoutPassword = Omit<User, 'password'>;
// Make all properties optional
type PartialUser = Partial<User>;
// Make all properties required
type RequiredUser = Required<User>;
11
// Make all properties readonly
type ReadonlyUser = Readonly<User>;
// Extract union type members
type UserRole = User['role']; // string literal union
Type Guards
Type guards narrow types based on runtime checks.
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
const data = await fetchData();
if (isUser(data)) {
// TypeScript knows data is User here
[Link]([Link]);
}
4. Performance Optimization
[Link]
[Link] prevents unnecessary re-renders of components. Components only
re-render when props change.
const UserCard = [Link]<UserCardProps>(({ name, email, avatar }) => {
[Link]('Rendering UserCard');
return (
<div className="user-card">
{avatar && <img src={avatar} alt={name} />}
<h3>{name}</h3>
<p>{email}</p>
</div>
);
});
Custom comparison functions provide fine-grained control:
12
const UserCard = [Link](
UserCardComponent,
(prevProps, nextProps) => {
return [Link] === [Link];
}
);
useMemo Hook
useMemo caches expensive computations. Values are recomputed only when
dependencies change.
const expensiveValue = useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]);
useCallback Hook
useCallback caches function references. Prevents passing new function instances
on every render.
const handleClick = useCallback(() => {
doSomething(a, b);
}, [a, b]);
<ChildComponent onClick={handleClick} />
Code Splitting
Code splitting loads components lazily, reducing initial bundle size.
const LazyComponent = lazy(() => import('./LazyComponent'));
const App = () => {
return (
<Suspense fallback={<Spinner />}>
<LazyComponent />
</Suspense>
);
};
Route-based splitting loads route components on demand:
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
<Routes>
<Route path="/" element={<Suspense fallback={<Spinner />}><Home /></Suspense>} />
13
<Route path="/about" element={<Suspense fallback={<Spinner />}><About /></Suspense>} />
</Routes>
Virtual Scrolling
Virtual scrolling renders only visible items in large lists, dramatically improving
performance.
import { FixedSizeList } from 'react-window';
const VirtualList = ({ items }: { items: User[] }) => {
const Row = ({ index, style }: { index: number; style: [Link] }) => (
<div style={style}>
<UserCard {...items[index]} />
</div>
);
return (
<FixedSizeList
height={600}
itemCount={[Link]}
itemSize={80}
width="100%"
>
{Row}
</FixedSizeList>
);
};
Image Optimization
Images are optimized through lazy loading, responsive images, and modern
formats.
const OptimizedImage: [Link]<{ src: string; alt: string }> = ({ src, alt }) => {
return (
<img
src={src}
alt={alt}
loading="lazy"
srcSet={`${src}?w=400 400w, ${src}?w=800 800w, ${src}?w=1200 1200w`}
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
/>
);
};
14
5. Testing Strategies
Component Testing with React Testing Library
React Testing Library tests components from user perspective. Tests verify
behavior, not implementation.
import { render, screen, fireEvent } from '@testing-library/react';
test('renders login form and handles submission', async () => {
const handleSubmit = [Link]();
render(<LoginForm onSubmit={handleSubmit} />);
const emailInput = [Link](/email/i);
const passwordInput = [Link](/password/i);
const submitButton = [Link]('button', { name: /log in/i });
[Link](emailInput, { target: { value: 'test@[Link]' } });
[Link](passwordInput, { target: { value: 'password123' } });
[Link](submitButton);
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@[Link]',
password: 'password123',
});
});
Testing Async Behavior
Async operations are tested with async utilities.
import { render, screen, waitFor } from '@testing-library/react';
test('loads and displays users', async () => {
render(<UserList />);
expect([Link](/loading/i)).toBeInTheDocument();
await waitFor(() => {
expect([Link]('Alice')).toBeInTheDocument();
expect([Link]('Bob')).toBeInTheDocument();
});
});
Testing Hooks
Custom hooks are tested with renderHook.
15
import { renderHook, act } from '@testing-library/react';
test('useCounter increments counter', () => {
const { result } = renderHook(() => useCounter());
expect([Link]).toBe(0);
act(() => {
[Link]();
});
expect([Link]).toBe(1);
});
Snapshot Testing
Snapshot tests catch unexpected UI changes.
test('UserCard matches snapshot', () => {
const { container } = render(
<UserCard name="Alice" email="alice@[Link]" />
);
expect([Link]).toMatchSnapshot();
});
End-to-End Testing
Cypress tests complete user workflows in real browsers.
describe('User Registration', () => {
it('allows users to register', () => {
[Link]('/register');
[Link]('[data-testid="name-input"]').type('Alice');
[Link]('[data-testid="email-input"]').type('alice@[Link]');
[Link]('[data-testid="password-input"]').type('password123');
[Link]('[data-testid="submit-button"]').click();
[Link]().should('include', '/dashboard');
[Link]('Welcome, Alice');
});
});
This extensive guide covers essential frontend patterns for building modern Re-
act applications with TypeScript. The patterns ensure maintainability, perfor-
mance, and type safety while following industry best practices.
16