React gives you a rendering model. TypeScript gives you a contract that catches shape mismatches before they reach production. Redux Toolkit gives you a single, predictable place for state that many unrelated components need to read and mutate. None of the three replaces the others — they solve different problems, and enterprise codebases combine all three because each one closes a failure mode the others leave open: React alone doesn't stop you from passing the wrong prop type, TypeScript alone doesn't organize where state lives, and neither alone gives you time-travel debugging or a single source of truth for data touched by a checkout flow, a nav bar badge, and an analytics panel simultaneously.
Choosing Where State Lives
The single most common architecture mistake in React codebases is putting everything in Redux, or putting everything in Context, out of habit rather than judgment. Each tool has a distinct cost profile.
| Tool | Scope | Update frequency it tolerates well | When to use |
|---|---|---|---|
| useState | Single component (or lifted to nearest common parent) | Any — isolated to the component that owns it | Form inputs, toggle flags, anything no sibling or distant component needs to read |
| Context API | A subtree, via a Provider | Low to moderate — every consumer re-renders on any value change unless split carefully | Thematic or rarely-changing data many components read: auth session, theme, locale |
| Redux Toolkit | Whole app (or a large feature slice) | High — selectors + memoization keep re-renders scoped to just the data a component reads | Complex, cross-cutting state: shopping carts, normalized API caches, undo/redo, anything with async lifecycles |
npm create vite@latest rtk-masterclass -- --template react-ts
cd rtk-masterclass
npm install
npm install @reduxjs/toolkit react-redux
npm install bootstrap bootstrap-icons
npm run devResulting Folder Architecture
rtk-masterclass/
├── src/
│ ├── app/
│ │ ├── store.ts # configureStore + typed RootState/AppDispatch
│ │ └── hooks.ts # useAppDispatch / useAppSelector
│ ├── features/
│ │ ├── cart/
│ │ │ └── cartSlice.ts
│ │ └── products/
│ │ └── productsSlice.ts
│ ├── context/
│ │ └── AuthContext.tsx
│ ├── components/
│ │ ├── NavBar.tsx
│ │ └── ProductGrid.tsx
│ ├── App.tsx
│ └── main.tsx
├── index.html
├── tsconfig.json
├── tsconfig.app.json
├── vite.config.ts
└── package.jsontsconfig.json and vite.config.ts
Vite's react-ts template splits TypeScript config into a root tsconfig.json (a project-references pointer) and tsconfig.app.json (the actual app compiler options). The settings that matter most for catching real bugs are strict and noUnusedLocals.
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["src"]
}import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
open: true
},
build: {
target: 'es2022',
sourcemap: true
}
})Wiring Bootstrap into main.tsx
Bootstrap's CSS must load before your own stylesheets (so your overrides win the cascade), and its JS bundle — required for interactive components like dropdowns and modals that rely on Popper.js internally — is a side-effect-only import.
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import { store } from './app/store'
import App from './App.tsx'
// Bootstrap CSS first, so component-level styles below can still override
// specific utility classes if ever needed.
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap-icons/font/bootstrap-icons.css'
// Bundle includes Popper.js — required for dropdowns, tooltips, popovers.
import 'bootstrap/dist/js/bootstrap.bundle.min.js'
import './index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>
)React.FC vs. Standard Inferred Typing
React.FC<Props> was the conventional way to type a functional component for years, but it has real drawbacks: it implicitly adds children to your props (even if the component never accepts children, which is incorrect in modern React types since v18) and it makes generics awkward to express. The current community and React team consensus is to type props as a plain interface and let TypeScript infer the return type.
import type { ReactNode } from 'react'
interface ProductCardProps {
id: string
name: string
price: number
tags: string[]
discountPercent?: number // optional field — may be undefined
onAddToCart: (productId: string) => void // typed callback prop
children?: ReactNode // explicit opt-in to accepting children, not implicit
}
// Preferred style: plain function, props typed via a destructured parameter.
// No React.FC wrapper — return type (JSX.Element) is inferred correctly.
function ProductCard({
id,
name,
price,
tags,
discountPercent,
onAddToCart,
children
}: ProductCardProps) {
const finalPrice = discountPercent
? price * (1 - discountPercent / 100)
: price
return (
<div className="card h-100 shadow-sm">
<div className="card-body">
<h5 className="card-title">{name}</h5>
<div className="mb-2">
{tags.map((tag) => (
<span key={tag} className="badge text-bg-secondary me-1">
{tag}
</span>
))}
</div>
<p className="card-text fw-bold">${finalPrice.toFixed(2)}</p>
{children}
<button
type="button"
className="btn btn-primary w-100"
onClick={() => onAddToCart(id)}
>
Add to Cart
</button>
</div>
</div>
)
}
export default ProductCardTyping Event Handlers
React wraps native DOM events in SyntheticEvent types that carry the same API but are pooled and normalized across browsers. Reaching for any on an event parameter throws away autocomplete on event.target.value, event.preventDefault(), and every other member — and it's almost never necessary, since React's own type definitions cover every native event.
import { useState, type ChangeEvent, type FormEvent } from 'react'
interface SearchFormProps {
onSearch: (query: string) => void
}
function SearchForm({ onSearch }: SearchFormProps) {
const [query, setQuery] = useState<string>('')
// ChangeEvent<HTMLInputElement> — event.target is narrowed to
// HTMLInputElement, so .value is typed as string without a cast.
function handleChange(event: ChangeEvent<HTMLInputElement>) {
setQuery(event.target.value)
}
// FormEvent<HTMLFormElement> — the generic parameter narrows
// event.currentTarget, though here we only need preventDefault().
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
if (query.trim().length === 0) return
onSearch(query.trim())
}
return (
<form className="d-flex gap-2" onSubmit={handleSubmit}>
<input
type="search"
className="form-control"
placeholder="Search products…"
value={query}
onChange={handleChange}
/>
<button type="submit" className="btn btn-outline-primary">
Search
</button>
</form>
)
}
export default SearchFormModule 1 Takeaways
- useState is for component-local data, Context is for low-frequency data shared by a subtree, Redux Toolkit is for high-frequency or complex cross-cutting state — pick based on update frequency and how many unrelated components need access.
- Context re-renders every consumer on any value change with no built-in selector layer; Redux Toolkit's useAppSelector scopes re-renders to just the slice a component reads.
- Vite's react-ts template + @reduxjs/toolkit + react-redux + bootstrap/bootstrap-icons is the full stack install — Bootstrap needs no @types package since it exposes no JS API to import.
- Bootstrap's CSS import must precede your own stylesheets; the bundled JS is a side-effect-only import required for interactive components.
- Prefer a plain typed-destructured-props function over React.FC — it avoids an implicit, always-present children prop and keeps generics simpler.
- Type event handlers with React's SyntheticEvent generics (ChangeEvent<HTMLInputElement>, FormEvent<HTMLFormElement>) instead of any — it's free autocomplete and correctness with no extra effort.