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.

ToolScopeUpdate frequency it tolerates wellWhen to use
useStateSingle component (or lifted to nearest common parent)Any — isolated to the component that owns itForm inputs, toggle flags, anything no sibling or distant component needs to read
Context APIA subtree, via a ProviderLow to moderate — every consumer re-renders on any value change unless split carefullyThematic or rarely-changing data many components read: auth session, theme, locale
Redux ToolkitWhole app (or a large feature slice)High — selectors + memoization keep re-renders scoped to just the data a component readsComplex, cross-cutting state: shopping carts, normalized API caches, undo/redo, anything with async lifecycles

terminalbash
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 dev

Resulting Folder Architecture

folder structuretext
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.json

tsconfig.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.

tsconfig.app.jsonjson
{
  "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"]
}
vite.config.tstypescript
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.

src/main.tsxtypescript
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.

src/components/ProductCard.tsxtypescript
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 ProductCard

Typing 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.

src/components/SearchForm.tsxtypescript
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 SearchForm

Module 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.