- 1 The usage of React, Redux, and TypeScript increases the quality of web development due to type safety and code maintainability. Set up with Create React App with TypeScript and add Redux for solid skeletons for a project.
- 2 Write state, Redux actions and reducers in TypeScript. The useSelector and useDispatch hooks are used to link the React components with the Redux store and offer type-safe access to the state as well as to dispatch actions.
- 3 Use Redux DevTools to investigate and optimize Redux application's log; Use Redux Toolkit to get cleaner Redux code. These tools improve development processes, minimize the amount of copy-paste code, and guarantee the presence of suitable mechanisms for state management.
React and Redux have become integral parts of modern web development, offering a powerful combination for building scalable and maintainable applications. When you introduce TypeScript into the mix, you enhance the development experience by adding static typing to your codebase. In this comprehensive guide, we’ll explore how React-Redux and TypeScript can be seamlessly integrated to create robust and efficient applications. Also read out the about redux toolkit and redux devTools.
Understanding the Basics
React is a JavaScript library for building user interfaces, developed and maintained by Facebook. It allows developers to build reusable UI components that update efficiently in response to data changes.
Redux: Redux is a state management library commonly used with React. It provides a predictable state container, making it easier to manage the state of your application and handle complex data flows.
TypeScript: TypeScript is a superset of JavaScript that adds static typing to the language. It enables developers to catch type-related errors during development, leading to more robust and maintainable code.
Why Pair TypeScript With Redux in the First Place?
It’s fair to ask whether adding TypeScript to a Redux setup is worth the extra ceremony. For small projects, plain JavaScript Redux is often fine. The case for TypeScript gets stronger as an app grows past a handful of components and more than one person starts touching the state layer.
A few concrete reasons teams make the switch:
- Action and reducer mismatches show up at compile time, not runtime. If a reducer expects
payload: stringand a component dispatches a number, TypeScript flags it before the code ships, rather than surfacing as an odd bug three sprints later. - Autocomplete gets meaningfully better. Once your state shape is typed, editors like VS Code can suggest the correct field names on
useSelectorcalls instead of you guessing or checking the reducer file each time. - Refactoring is safer. Rename a field in your state interface and TypeScript will point to every place that needs updating. Skip that step in plain JavaScript and you’re relying on search-and-replace or, worse, manual testing to catch what broke.
- The types double as documentation. A new developer reading
AppStategets a fairly accurate picture of what data the app tracks, without needing to trace through every reducer.
Setting Up Your Project
1. Initializing a React App with TypeScript
To start a new React app with TypeScript, you can use Create React App (CRA) with the TypeScript template. Run the following command:
npx create-react-app my-app --template typescript
cd my-app2. Adding Redux to the Project
To integrate Redux into your project, install the required packages:
npm install redux react-redux @types/react-reduxImplementing Redux with TypeScript
1. Creating the Redux Store
Define the initial state, actions, and reducers for your application. Use TypeScript to add type annotations and ensure type safety.
// src/store/types.ts
export interface AppState {
// Define your application state here
}
// src/store/actions.ts
export enum ActionTypes {
// Define your action types here
}
// src/store/reducers.ts
import { ActionTypes } from './actions';
import { AppState } from './types';
const initialState: AppState = {
// Initialize your state properties here
};
const rootReducer = (state: AppState = initialState, action: AnyAction): AppState => {
switch (action.type) {
// Handle different action types and update state accordingly
default:
return state;
}
};
export default rootReducer;2. Connecting Redux with React Components
Use the useSelector and useDispatch hooks from react-redux to connect your React components to the Redux store.
// src/components/ExampleComponent.tsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { AppState } from '../store/types';
const ExampleComponent: React.FC = () => {
const exampleData = useSelector((state: AppState) => state.exampleData);
const dispatch = useDispatch();
// Dispatch actions as needed
return (
<div>
{/* Render your component using the Redux state */}
</div>
);
};
export default ExampleComponent;Adding TypeScript to Redux
1. Typing Actions
Define action types and payloads using TypeScript for better type safety.
// src/store/actions.ts
export enum ActionTypes {
SET_DATA = 'SET_DATA',
}
interface SetDataAction {
type: ActionTypes.SET_DATA;
payload: string;
}
export type Action = SetDataAction;
// src/store/reducers.ts
const rootReducer = (state: AppState = initialState, action: Action): AppState => {
switch (action.type) {
case ActionTypes.SET_DATA:
return {
...state,
exampleData: action.payload,
};
default:
return state;
}
};
2. Typing the Redux Store
Create a strongly-typed store by combining the reducers and using the createStore function from Redux.
// src/store/index.ts
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
export type RootState = ReturnType<typeof rootReducer>;
export default store;Best Practices for a Type-Safe Redux Setup
Once the basic typing is in place, a handful of habits keep things from getting messy as the application grows.
The first is to infer types rather than writing them out twice. Rather than manually maintaining a separate interface alongside your reducer to describe the shape of the store, it’s better to derive the root state type directly from the reducer itself, the way this guide does earlier using TypeScript’s return-type inference. The same idea applies to the dispatch function’s type — deriving it from the store instance means it always matches reality, rather than needing to be updated by hand every time a new piece of middleware or a new reducer is added.
The second habit is creating typed versions of the standard hooks instead of typing the state shape inline every time you read from the store or dispatch an action. Doing this once, in a single shared file, and importing those typed hooks everywhere else in the app means nobody has to remember to write out the type annotation by hand in every component. It also means that if the state shape changes, only one file needs updating rather than every component that happens to read from the store.
Third, it helps to avoid reaching for a loose “any” type on action payloads, even when a payload’s shape is still being worked out. It’s a tempting shortcut early in development, but it tends to spread — one untyped payload in a reducer often leads to a few more appearing in the components that consume it, and before long a meaningful portion of the store has quietly lost its type safety.
Fourth, model your actions as a discriminated union, the way this guide’s example does with its “set data” action, rather than as one loose interface with a lot of optional fields. This is what allows a switch statement inside a reducer to correctly narrow the payload type for each individual case, which is one of the more genuinely useful things TypeScript does for Redux code specifically.
Finally, keep the state shape as flat as is reasonably possible. Deeply nested state is harder to type cleanly, harder to update immutably without introducing subtle bugs, and harder for a new team member to reason about. If a slice of state starts getting more than two or three levels deep, that’s usually a sign it would be better split into its own separate reducer rather than nested further.
What are Redux DevTools and Redux Toolkit ?
1. Redux DevTools
Redux DevTools is a browser extension and a middleware for Redux that enhances the debugging capabilities of Redux applications. It provides a visual representation of the state and actions, allowing developers to inspect, trace, and debug their application’s state changes. With features like time-travel debugging, developers can move backward and forward through the application’s state history, making it easier to identify and fix issues. DevTools is an invaluable tool for improving the development and debugging experience when working with Redux.
2. Redux Toolkit
Redux Toolkit is an opinionated set of utilities and conventions designed to simplify and optimize the development process when using Redux for state management in React applications. It includes functions like createSlice for reducer creation, configureStore for store setup, and other tools to help developers write more efficient and maintainable Redux code. The toolkit aims to reduce boilerplate code and encourage best practices, making it easier to build scalable and robust applications.
When Redux Might Not Be the Right Tool, Typed or Not
It’s worth stepping back from the setup details for a moment, because typed Redux is genuinely powerful for the right kind of application, but it isn’t the right choice for every React project, and this guide would be incomplete without saying so.
Redux earns its keep when state needs to be shared across many components that aren’t directly related to each other in the component tree — a shopping cart that’s read from a header icon, a product page, and a checkout flow all at once, for example, or a notification system that any part of the app can trigger. It also tends to justify itself when a team specifically wants the debugging tools Redux DevTools provides, including the ability to step backward through past state changes to see exactly what happened and in what order.
On the other hand, if most of an application’s state is genuinely local — form inputs, whether a modal is open, which tab is currently selected — routing that through Redux usually adds indirection without adding much real benefit. React’s own useState and useReducer hooks, sometimes paired with the Context API for state that a handful of related components need to share, cover a large share of what people reach for Redux to do, especially in applications built from scratch since hooks became the standard way to manage state in React.
Server data is its own separate case worth mentioning directly. A lot of what used to live in Redux stores — API responses, cached data, loading and error states for network requests — is now often handled better by a dedicated data-fetching library instead. Tools built specifically for that purpose handle caching, retries, and cache invalidation in ways that a hand-rolled Redux slice usually doesn’t, without much extra code required to get equivalent behavior. In practice, many modern applications end up using Redux for genuinely global client state — things like authentication status, UI preferences, or the shopping-cart example above — while letting a separate tool handle anything that originates from a server.
None of this is an argument against the setup described earlier in this guide. It’s simply a reminder that the decision to add Redux, and to type it thoroughly with TypeScript, is worth making deliberately, based on what a particular application actually needs, rather than by default because it’s a familiar pattern from a previous project.
Common Mistakes to Watch For
A handful of issues come up often enough in React, Redux, and TypeScript projects that they’re worth calling out on their own.
One of the most common is typing the state-reading hook inconsistently across a codebase. If some components use a properly typed version of the hook and others still call the untyped one with an inline annotation, a lot of the benefit gets lost, and it becomes easy for new code written later to slip back into the untyped pattern simply out of habit, since both approaches technically work.
Another frequent issue is storing the same piece of data in both local component state and in the Redux store at the same time. A common example is a form field that lives in local state while the user is typing, and also gets pushed into Redux on every keystroke. Beyond the performance cost of the extra re-renders this causes, it creates two separate sources of truth that can drift out of sync with each other. As a general rule, state that’s only relevant to a single component belongs in local state, and only gets promoted into Redux once another, unrelated part of the application actually needs to read it.
Forgetting to properly type asynchronous logic is another common gap. If a project uses thunks or similar middleware to handle asynchronous actions, that logic needs its own type signatures too. Skipping this step is a frequent source of untyped code sneaking back into an otherwise well-typed application, since async logic is often written under time pressure and typing it correctly takes a bit more thought than typing a simple synchronous action.
Over-normalizing small pieces of state is a subtler mistake, but a real one. Normalizing data — storing entities by their ID in a lookup table rather than as a plain array — is genuinely useful for large, relational datasets with lots of cross-references. Applying the same pattern to a small settings object with three or four fields adds typing overhead and indirection with very little actual payoff. It’s worth matching the pattern to the real complexity of the data rather than applying it everywhere by default.
Last, some teams skip Redux Toolkit entirely and hand-write everything the way this guide does for teaching purposes. That’s a reasonable way to learn what’s happening underneath, but for production code, letting Redux Toolkit generate the action types and typed reducers removes a lot of the room for the exact mistakes described above, simply because there’s less hand-written boilerplate for a mistake to hide in.
Conclusion
In this guide, we’ve covered the basics of integrating React-Redux with TypeScript, starting with a plain Redux setup and building up to a fully typed store, typed actions, and typed hooks for reading and updating state. Along the way, we also looked at where Redux Toolkit and Redux DevTools fit in, the habits that keep a typed store manageable as an application grows, and a few of the mistakes that tend to show up in real projects once more than one person is working in the same codebase.
The core idea worth carrying forward is that TypeScript and Redux solve two different, complementary problems. Redux gives an application a single, predictable place for state that needs to be shared across components that aren’t directly connected to each other. TypeScript, layered on top, catches an entire category of mistakes — mismatched action payloads, incorrect field names, state shapes that drift out of sync with what components expect — before that code ever reaches a user. Neither one replaces good judgment about what actually belongs in global state versus what should stay local to a component, which is a decision worth revisiting as an application grows rather than something to settle once at the start of a project.
Getting comfortable with this setup takes some repetition. The first typed store in a new codebase is usually the slowest one to put together, since it involves working through generics, discriminated unions, and inferred types that might be unfamiliar if you’re coming from plain JavaScript Redux. That upfront effort tends to pay off fairly quickly, though, particularly on any project more than a few people will touch, or one that’s expected to stick around long enough for the original author to forget the exact shape of the state a few months later.
From here, a reasonable next step is to work through Redux Toolkit’s createSlice directly in a small project, since it removes a lot of the manual typing shown in this guide’s earlier examples while keeping the same underlying ideas. After that, middleware, async actions handled through thunks, and writing tests against a typed store are all natural extensions of what’s covered here, and each one builds directly on the typed foundation this guide walks through. None of them require rethinking the approach — they’re additions to it.
