DEV Community

Vikash Kumar
Vikash Kumar

Posted on • Originally published at Medium on

React + Redux Toolkit QuickStart Guide

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
Enter fullscreen mode Exit fullscreen mode

JavaScript:

npm create vite@latest my-redux-app -- --template react
cd my-redux-app
npm install
Enter fullscreen mode Exit fullscreen mode

Install Redux Toolkit and React Redux:

npm install @reduxjs/toolkit react-redux
Enter fullscreen mode Exit fullscreen mode

Start the dev server:

npm run dev
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

JavaScript:

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';

export const store = configureStore({
  reducer: { counter: counterReducer },
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Add to store:

import postsReducer from './features/posts/postsSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    posts: postsReducer,
  },
});
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Testing a slice quickly (Vitest)

With Vite, Vitest is easy to add:

npm install -D vitest @testing-library/react @testing-library/jest-dom
Enter fullscreen mode Exit fullscreen mode

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 });
});
Enter fullscreen mode Exit fullscreen mode

Run tests:

npx vitest run
Enter fullscreen mode Exit fullscreen mode

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)