In last part we built the Store. But your React components have no idea it exists.
A component can't just reach into the store directly. React and Redux are two separate worlds. Something has to connect them. That's what this article is about the bridge.
Two things need to happen. The store needs to be injected into the React tree. And every component that talks to Redux needs a safe, typed way to do it.
Why Not Just Use the Raw Hooks?
React-Redux ships with useSelector and useDispatch. They work out of the box.
But they're untyped.
useDispatch() returns a generic dispatch function. useSelector doesn't know what your state looks like. Every component would need to import RootState and AppDispatch and wire the types manually, every single time. That's repetition.
Instead, we create typed wrappers once in hooks.ts and then every component will import from there.
Create a file name hooks.ts under store folder and add following code.
import { useDispatch, useSelector, type TypedUseSelectorHook } from "react-redux";
import { AppDispatch, RootState } from "./store";
type DispatchFunc = () => AppDispatch;
export const useCartDispatch: DispatchFunc = useDispatch;
export const useCartSelector: TypedUseSelectorHook<RootState> = useSelector;
we saw in last article that store exports, RootState and AppDispatch, where RootState is for reading and AppDispatch is for writing, now we're simply wrapping the raw hooks with our own types so every component gets type safety for free.
useCartDispatch for writing.
useCartSelector for reading.
TypedUseSelectorHook comes from React-Redux. It's a generic that wraps useSelector with your specific state type.
Check out the full implementation and give the repo a star on redux-simplified
The UI Shell
The store is wired. The hooks exist. And we need to place the Provider is in place. This is the right moment to look at what's waiting on the UI side. Redux is the goal. But we're building a real app, so there are components too. As per our structure we are done with store, now go ahead and create folder named "components" and we will add components one by one.
src/
├── App.tsx
├── dummy-products.ts
├── components/
│ ├── Header.tsx
│ ├── Product.tsx
│ ├── Cart.tsx
│ ├── CartItems.tsx
│ └── Shop.tsx
└── store/
├── store.ts
├── cart-slice.ts
└── hooks.ts
You don't need to master these right now. This is just orientation, a map of the UI so nothing surprises you in later articles. Each component is a placeholder today. Each one will come alive as we wire in Redux. Let's start with Index.html.
1. Index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Redux + Typescript</title>
</head>
<body>
<div id="modal"></div>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Two Divs That Matter
<div id="modal"></div>
<div id="root"></div>
There are two divs. root is standard, React mounts your entire app here. But modal is separate, sitting above it in the DOM. This is where the cart dialog will render. Not inside the component tree. Above it.
2. Cart.tsx: The Portal
import { createPortal } from 'react-dom';
import CartItems from './CartItems.tsx';
type CartProps = {
onClose: () => void;
};
export default function Cart({ onClose }: CartProps) {
return createPortal(
<>
<div className="cart-backdrop" />
<dialog id="cart-modal" open>
<h2>Your Cart</h2>
<CartItems />
<p id="cart-actions">
<button onClick={onClose}>Close</button>
</p>
</dialog>
</>,
document.getElementById('modal')!
);
}
The cart renders as a <dialog>a proper HTML modal element. But notice the second argument to createPortal: document.getElementById('modal'). That's the div, I was referring to in index.html.
Why does this matter? If the cart rendered inside the normal component tree, it would inherit CSS stacking contexts from its parents. Overlays would break. By portaling to #modal which sits outside #root entirely the cart floats above everything, without CSS battles.
onClose is a prop for now. Later, it won't need to be. The store will handle visibility state.
3. CartItems.tsx: Commented On Purpose
export default function CartItems() {
return (
<></>
// <div id="cart">
// {
// cartItems.length === 0 &&
// <p>No items in cart!</p>
// }
// {cartItems.length > 0 && (
// <ul id="cart-items">
// {[cartItems.map](http://cartItems.map)((item) => {
// const formattedPrice = $${item.price.toFixed(2)};
// return (
// <li key={[item.id](http://item.id)}>
// <div>
// <span>{item.title}</span>
// <span> ({formattedPrice})</span>
// </div>
// <div className="cart-item-actions">
// <button onClick={() => handleRemoveFromCart([item.id](http://item.id))}>
// -
// </button>
// <span>{item.quantity}</span>
// <button onClick={() => handleAddToCart(item)}>+</button>
// </div>
// </li>
// );
// })}
// </ul>
// )}
// <p id="cart-total-price">
// Cart Total: <strong>{formattedTotalPrice}</strong>
// </p>
// </div>
);
}
This component is intentionally empty right now. The commented code inside is the finished cart, items, quantities, add/remove buttons, total price. It's all there, waiting. But every line of that logic depends on Redux hooks that we yet to map.
4. Header.tsx: The Cart Button That Does Nothing (Yet)
import Cart from './Cart.tsx';
export default function Header() {
function handleOpenCartClick() {}
return (
<>
<header id="main-header">
<div id="main-title">
<h1>learn redux + Typescript with cart feature</h1>
</div>
<p>
<button onClick={handleOpenCartClick}>Cart ({0})</button>
</p>
</header>
</>
);
}
Two things to notice.
handleOpenCartClick is empty. The button renders. Nothing opens. That's temporary.
Cart ({0}) the count is hardcoded to zero. That zero will eventually be replaced by a selector reading from the store. When it is, the header will always reflect the real cart count. Automatically. Without any prop passing.
5. Product.tsx: The Add Button That Does Nothing (Yet)
type ProductProps = {
id: string;
image: string;
title: string;
price: number;
description: string;
};
export default function Product({ id, image, title, price, description }: ProductProps) {
function handleAddToCart() {}
return (
<article className="product">
<img src={image} alt={title} />
<div className="product-content">
<div>
<h3>{title}</h3>
<p className="product-price">${price}</p>
<p>{description}</p>
</div>
<p className="product-actions">
<button onClick={handleAddToCart}>Add to Cart</button>
</p>
</div>
</article>
);
}
ProductProps defines exactly what this component needs, id, image, title, price, description. Note: You can export it from from slice if you want.
handleAddToCart is empty. The button is there. The click handler is there. The only missing piece is dispatch(addToCart(...))one line that connects this button to the store. That line arrives in Article 4.
6. Shop.tsx : The Container
import { type ReactNode } from 'react';
type ShopProps = {
children: ReactNode;
};
export default function Shop({ children }: ShopProps) {
return (
<section id="shop">
<h2>Performance Auto Parts Catalog</h2>
<ul id="products">{children}</ul>
</section>
);
}
Shop is a layout wrapper. It doesn't know what products exist. It just renders whatever is passed as children inside a <ul>.
ReactNode is the TypeScript type for "anything React can render", JSX, strings, arrays, fragments. Using it here keeps Shop flexible.
7. dummy-products.ts: The Product Data
import discBrakeRotor from './assets/disc-brake-rotor.png';
import aluminumOilPan from './assets/aluminum-oil-pan.png';
import macphersonStrutAssembly from './assets/macpherson-strut-assembly.png';
import sixSpeedTransmissionCase from './assets/six-speed-manual-transmission-case.png';
import cylinderHeadGasket from './assets/cylinder-head-gasket.png';
import aluminumPiston from './assets/aluminum-piston.png';
export const DUMMY_PRODUCTS = [
{
id: 'p1',
image: discBrakeRotor,
title: 'High-Performance Disc Brake Rotor',
price: 189.99,
description:
'Cross-drilled and slotted performance brake rotor with anti-corrosion hub coating. Designed for reliable stopping power and heat management in high-demand driving.',
},
{
id: 'p2',
image: aluminumOilPan,
title: 'Die-Cast Aluminum Oil Pan',
price: 149.99,
description:
'Robust die-cast aluminum oil pan with cooling fins and precision bolt-hole pattern. Built for durability, thermal control, and leak-resistant sealing performance.',
},
{
id: 'p3',
image: macphersonStrutAssembly,
title: 'Complete MacPherson Strut Assembly',
price: 259.99,
description:
'Front suspension strut assembly including spring, hydraulic shock absorber, and top mount. Engineered for stable handling, ride comfort, and reliable damping.',
},
{
id: 'p4',
image: sixSpeedTransmissionCase,
title: 'Six-Speed Manual Transmission Case',
price: 1299.99,
description:
'Complete manual six-speed transaxle unit with ribbed aluminum bell housing and main case. Built to handle drivetrain loads with precise shift-linkage integration.',
},
{
id: 'p5',
image: cylinderHeadGasket,
title: 'Multi-Layer Steel Cylinder Head Gasket',
price: 89.99,
description:
'Precision MLS gasket with integrated fire rings and stamped sealing layers. A critical replacement part for head sealing integrity under high temperature and pressure.',
},
{
id: 'p6',
image: aluminumPiston,
title: 'Aluminum Piston and Connecting Rod Assembly',
price: 119.99,
description:
'A single, brand-new piston and connecting rod assembly, made of durable steel and aluminum, is displayed against a dark gray gradient background. This critical engine component is responsible for transforming expanding gases into mechanical energy. It is clean, gleaming under bright studio light, and rests on a reflective dark surface.',
},
];
In folder structure, you'll see there is one file named "dummy-products". This file is the store's inventory. Six car parts, each with an id, image, title, price, and description.
Notice the shape matches ProductProps in Product.tsx exactly. That's intentional. When App.tsx spreads {...product} onto each <Product />, TypeScript verifies every field lines up.
We are working static data, so we can focus entirely on Redux without network complexity getting in the way.
Note: Images you can simply download from git repo: redux-simplified
7. App.tsx: Where It All Comes Together
import Header from './components/Header.tsx';
import Shop from './components/Shop.tsx';
import Product from './components/Product.tsx';
import { DUMMY_PRODUCTS } from './dummy-products.ts';
function App() {
return (
<>
<Header />
<Shop>
{DUMMY_PRODUCTS.map((product) => (
<li key={product.id}>
<Product {...product} />
</li>
))}
</Shop>
</>
);
}
App pulls in the dummy product data and maps over it, one <Product> per item, each wrapped in an <li> for the <ul> inside Shop. The spread {...product} passes every field of the product object as individual props, matching the ProductProps type exactly.
Header sits above everything. Shop holds the product grid below.
This is the whole app at this stage. It renders. It looks like a store. Nothing interactive works yet.
Provider: Injecting the Store
Provider makes the store available to every component in the app.
Without Provider, any component that calls useCartSelector or useCartDispatch will throw. It can't find the store. The connection doesn't exist.
import { Provider } from 'react-redux';
import { store } from './store/store';
function App() {
return (
<Provider store={store}>
<Header />
<Shop>
{DUMMY_PRODUCTS.map((product) => (
<li key={product.id}>
<Product {...product} />
</li>
))}
</Shop>
</Provider>
);
}
Provider wraps everything. Header, Shop, Product, Cart, CartItems, every component in this tree can now reach the store through hooks.
This is the only place store gets imported into the UI layer. App.tsx hands it to Provider. Provider handles the rest. Components never import the store directly, they use hooks.
The foundation is complete. Every piece is in place.
What's missing is the moment a user clicks "Add to Cart" and the store actually responds.
That's next.
Previous: Part 2: Slice and Store
Redux Simplified: The Complete Series
This series walks you through Redux from scratch by building a real car parts e-commerce store in React + TypeScript.
Redux simplified part 1: Fundamentals
Redux simplified part 2: Slice and Store
Redux simplified part 3: Custom Hooks and UI Shell
Redux simplified part 4: Wring and Reading state
If you want to see the 'moving parts' in action, the code is waiting for you on redux-simplified