This guide gives you a concise Redux overview, a fresh React app setup, and practical examples of using Redux Toolkit in React — including installation steps.
Why Redux (and why Redux Toolkit)
Redux is a predictable state container for Typescript/JavaScript apps. It centralizes shared state in a single store and uses pure reducers to handle updates.
Core pieces
- Store: the single source of truth for app state
- Actions: plain objects describing “what happened”
- Reducers: pure functions that update state based on actions
- Dispatch: sends actions to the store
- Selectors: read from the store (often memoized)
- Middleware: intercept actions for side effects (logging, async, etc.)
Redux Toolkit (RTK) is the official, recommended way to write Redux logic:
- APIs included: configureStore, createSlice, createAsyncThunk
- Eliminates boilerplate and enforces good defaults
- Uses Immer under the hood for safe immutable updates with mutable-looking code
- Built-in dev tools compatibility
When to use Redux:
- You have shared state across many components (auth, user prefs, cached lists)
- You need predictable state transitions and time-travel debugging
- You want a single place for async data fetching/caching and error handling
If your app is small or state is mostly local, React state or Context might be enough. For server data, consider RTK Query (Redux Toolkit’s data fetching solution).
Set up a sample React application
Prerequisites
- Node.js 18+ (recommended)
- npm 9+ (or yarn/pnpm)
Create a new project with Vite
TypeScript (recommended):
npm create vite@latest my-redux-app -- --template react-ts
cd my-redux-app
npm install
JavaScript:
npm create vite@latest my-redux-app -- --template react
cd my-redux-app
npm install
Install Redux Toolkit and React Redux:
npm install @reduxjs/toolkit react-redux
Start the dev server:
npm run dev
Open the URL printed in the terminal (typically http://localhost:5173)..)
Using Redux Toolkit in React
Below are minimal, production-ready patterns using Redux Toolkit.
1) Create a slice (src/features/counter/counterSlice.ts or .js)
TypeScript version:
import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
// Example async thunk
export const fetchInitial = createAsyncThunk<number>(
'counter/fetchInitial',
async () => {
// Mock API call; replace with real endpoint
await new Promise((r) => setTimeout(r, 300));
return 5;
}
);
export interface CounterState {
value: number;
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error?: string;
}
const initialState: CounterState = {
value: 0,
status: 'idle',
};
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment(state) {
state.value += 1; // safe with Immer
},
decrement(state) {
state.value -= 1;
},
addByAmount(state, action: PayloadAction<number>) {
state.value += action.payload;
},
reset(state) {
state.value = 0;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchInitial.pending, (state) => {
state.status = 'loading';
state.error = undefined;
})
.addCase(fetchInitial.fulfilled, (state, action) => {
state.status = 'succeeded';
state.value = action.payload;
})
.addCase(fetchInitial.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
},
});
export const { increment, decrement, addByAmount, reset } = counterSlice.actions;
export default counterSlice.reducer;
JavaScript version:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchInitial = createAsyncThunk('counter/fetchInitial', async () => {
await new Promise((r) => setTimeout(r, 300));
return 5;
});
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0, status: 'idle' },
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; },
addByAmount: (state, action) => { state.value += action.payload; },
reset: (state) => { state.value = 0; },
},
extraReducers: (builder) => {
builder
.addCase(fetchInitial.pending, (state) => { state.status = 'loading'; })
.addCase(fetchInitial.fulfilled, (state, action) => {
state.status = 'succeeded';
state.value = action.payload;
})
.addCase(fetchInitial.rejected, (state, action) => {
state.status = 'failed';
});
},
});
export const { increment, decrement, addByAmount, reset } = counterSlice.actions;
export default counterSlice.reducer;
2) Configure the store (src/store.ts or .js)
TypeScript:
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
// Infer types
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
JavaScript:
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';
export const store = configureStore({
reducer: { counter: counterReducer },
});
3) Typed hooks (optional but recommended for TS)
src/hooks.ts:
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
4) Wrap your app with the Provider (src/main.tsx)
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './store';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
JavaScript (main.jsx) is similar.
5) Use Redux in a component (src/features/counter/Counter.tsx)
import { useState } from 'react';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { increment, decrement, addByAmount, reset, fetchInitial } from './counterSlice';
export default function Counter() {
const value = useAppSelector((state) => state.counter.value);
const status = useAppSelector((state) => state.counter.status);
const dispatch = useAppDispatch();
const [amount, setAmount] = useState('3');
return (
<div style={{ fontFamily: 'system-ui', lineHeight: 1.6 }}>
<h2>Counter: {value}</h2>
<p>Status: {status}</p>
<button onClick={() => dispatch(increment())}>+1</button>
<button onClick={() => dispatch(decrement())} style={{ marginLeft: 8 }}>-1</button>
<button onClick={() => dispatch(reset())} style={{ marginLeft: 8 }}>Reset</button>
<div style={{ marginTop: 12 }}>
<input
value={amount}
onChange={(e) => setAmount(e.target.value)}
style={{ width: 60 }}
/>
<button onClick={() => dispatch(addByAmount(Number(amount) || 0))} style={{ marginLeft: 8 }}>
Add by amount
</button>
</div>
<div style={{ marginTop: 12 }}>
<button onClick={() => dispatch(fetchInitial())}>Fetch initial</button>
</div>
</div>
);
}
Add to ** App.tsx:**
import Counter from './features/counter/Counter';
export default function App() {
return (
<main style={{ padding: 24 }}>
<h1>My Redux App</h1>
<Counter />
</main>
);
}
Async example: fetching posts with createAsyncThunk
src/features/posts/postsSlice.ts:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchPosts = createAsyncThunk('posts/fetch', async () => {
const res = await fetch('https://server.vikash.com/posts?_limit=5');
if (!res.ok) throw new Error('Failed to fetch');
return (await res.json()) as Array<{ id: number; title: string }>;
});
const postsSlice = createSlice({
name: 'posts',
initialState: { items: [], status: 'idle', error: undefined } as {
items: Array<{ id: number; title: string }>;
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error?: string;
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => { state.status = 'loading'; state.error = undefined; })
.addCase(fetchPosts.fulfilled, (state, action) => { state.status = 'succeeded'; state.items = action.payload; })
.addCase(fetchPosts.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message; });
},
});
export default postsSlice.reducer;
Add to store:
import postsReducer from './features/posts/postsSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
posts: postsReducer,
},
});
Use in a component:
import { useAppDispatch, useAppSelector } from '../../hooks';
import { fetchPosts } from './postsSlice';
export default function Posts() {
const { items, status, error } = useAppSelector((s) => s.posts);
const dispatch = useAppDispatch();
return (
<section>
<h2>Posts</h2>
<button disabled={status === 'loading'} onClick={() => dispatch(fetchPosts())}>
{status === 'loading' ? 'Loading...' : 'Load posts'}
</button>
{error && <p style={{ color: 'crimson' }}>{error}</p>}
<ul>
{items.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
</section>
);
}
Testing a slice quickly (Vitest)
With Vite, Vitest is easy to add:
npm install -D vitest @testing-library/react @testing-library/jest-dom
Example test src/features/counter/counterSlice.test.ts:
import reducer, { increment, decrement, addByAmount } from './counterSlice';
test('counter reducers', () => {
const s = { value: 0, status: 'idle' };
expect(reducer(s, increment())).toMatchObject({ value: 1 });
expect(reducer(s, decrement())).toMatchObject({ value: -1 });
expect(reducer(s, addByAmount(10))).toMatchObject({ value: 10 });
});
Run tests:
npx vitest run
Common pitfalls & troubleshooting
- State appears unchanged: ensure you used reducers from createSlice and dispatched the correct action.
- useSelector returns undefined: verify the reducer key in configureStore matches usage (e.g., state.counter).
- Async thunk “pending” forever: check network errors and ensure you return a value from the thunk.
- DevTools not showing actions: confirm you’re using configureStore (it enables Redux DevTools by default in dev).
- TypeScript types not inferred: export RootState and AppDispatch, and use typed hooks.
Next steps
- RTK Query for data fetching/caching with normalized state and auto hooks.
- Persisted state (redux-persist) for auth/session.
- Feature-based folder structure (src/features/*) to keep slices and components together.
- Advanced selectors with Reselect for derived state.
- Add ESLint + Prettier for consistent code quality.
Quick reference
- Install: npm i @reduxjs/toolkit react-redux
- Slice: createSlice({ name, initialState, reducers })
- Async: createAsyncThunk(type, asyncFn)
- Store: configureStore({ reducer })
- Provider:
- Hooks: useSelector, useDispatch (typed variants recommended)

Top comments (0)