Skill

Generate Redux Slices with TypeScript

Skill for Redux Toolkit slices - createSlice, createAsyncThunk, normalized state, and memoized selectors.

Maintainer of this project? Claim this page to edit the listing.


91
Spark score
out of 100
Updated 7 months ago
Version 1.0.0
Models

Add to Favorites

Why it matters

Automate the creation of clean, type-safe Redux slices using Redux Toolkit. This asset ensures adherence to modern Redux patterns, including immutable updates, proper error handling, and efficient state management for both synchronous and asynchronous operations.

Outcomes

What it gets done

01

Generate Redux slices using `createSlice`

02

Implement TypeScript types for state and actions

03

Create async thunks with error and loading states

04

Incorporate normalized state patterns with `createEntityAdapter`

Install

Add it to your toolbox

Run in your project directory:

curl -fsSL https://spark.entire.vc/get/vb-redux-slice-generator | bash

Overview

Redux Slice Generator

A skill for Redux Toolkit slices - typed state and actions, async CRUD thunks with rejectWithValue error handling, normalized state via createEntityAdapter, and memoized selectors. Use it once Redux Toolkit is the chosen state-management approach, not for selecting a state-management library or plain Redux without RTK.

What it does

This skill generates clean, type-safe Redux Toolkit slices - createSlice, createAsyncThunk, and proper state-management patterns, including TypeScript typing for all state, actions, and payloads, immutable updates via Immer (built into RTK), and a clear separation of synchronous and asynchronous actions. A slice template defines an EntityState interface (items, selectedItem, loading, error, filters), a typed initial state, and an async thunk:

export const fetchEntities = createAsyncThunk(
  'entities/fetchEntities',
  async (params: { page?: number; limit?: number } = {}, { rejectWithValue }) => {
    try {
      const response = await api.getEntities(params);
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response?.data?.message || 'Failed to fetch entities');
    }
  }
);

The slice itself wires synchronous reducers (setSelectedItem, updateFilter, clearError, resetState) alongside extraReducers handling the thunk's pending/fulfilled/rejected cases. CRUD-operation patterns cover create, update, and delete thunks, each using rejectWithValue for consistent error handling. Advanced state patterns cover normalized state via createEntityAdapter (with a selectId and sortComparer, and adapter methods setAll, updateOne, removeOne used inside extraReducers), and selectors - basic state selectors plus memoized selectors built with createSelector that filter and sort entities by search text, category, and sort key.

When to use - and when NOT to

Use it when generating or reviewing a Redux Toolkit slice - typed state and actions, async thunks for API calls, normalized collections, or memoized selectors. It assumes Redux Toolkit is already the state-management choice - it is not a guide to choosing a state-management library or to plain Redux without RTK.

Inputs and outputs

Given an entity or domain to manage, it produces a typed EntityState interface, a createSlice definition with reducers and extraReducers, CRUD async thunks, and memoized selectors.

Best-practice guidance covers naming actions with a domain/action pattern, providing sensible initial-state defaults to avoid undefined states, including reset/clear actions for state management, and grouping related slices in feature directories. Configuration tips also cover exporting both actions and selectors from each slice file, using consistent naming patterns across slices, adding middleware for logging in development, and using the Redux DevTools extension for debugging state changes.

Integrations

Built on Redux Toolkit (createSlice, createAsyncThunk, createEntityAdapter, createSelector) with Immer for immutable updates and TypeScript for typing; also notes RTK Query as an option for complex API interactions and Redux DevTools for debugging.

Who it's for

Frontend developers building or maintaining Redux Toolkit state slices in a TypeScript codebase, from a single feature slice to a normalized multi-entity store.

Source README

Redux Slice Generator

You are an expert in Redux Toolkit and modern Redux patterns, specializing in generating clean, maintainable, and type-safe Redux slices. You understand the intricacies of createSlice, createAsyncThunk, and proper state management patterns.

Core Principles

  • Always use Redux Toolkit's createSlice for slice generation
  • Implement proper TypeScript typing for all state, actions, and payloads
  • Follow immutable update patterns using Immer (built into RTK)
  • Structure slices with clear separation of synchronous and asynchronous actions
  • Include proper error handling and loading states for async operations
  • Use descriptive action names that clearly indicate their purpose
  • Implement proper initial state with sensible defaults

Slice Structure Template

import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';

// Types
interface EntityState {
  items: Entity[];
  selectedItem: Entity | null;
  loading: boolean;
  error: string | null;
  filters: FilterState;
}

interface Entity {
  id: string;
  name: string;
  // ... other properties
}

interface FilterState {
  search: string;
  category: string;
  sortBy: 'name' | 'date' | 'priority';
}

// Initial State
const initialState: EntityState = {
  items: [],
  selectedItem: null,
  loading: false,
  error: null,
  filters: {
    search: '',
    category: 'all',
    sortBy: 'name'
  }
};

// Async Thunks
export const fetchEntities = createAsyncThunk(
  'entities/fetchEntities',
  async (params: { page?: number; limit?: number } = {}, { rejectWithValue }) => {
    try {
      const response = await api.getEntities(params);
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response?.data?.message || 'Failed to fetch entities');
    }
  }
);

// Slice
const entitySlice = createSlice({
  name: 'entities',
  initialState,
  reducers: {
    setSelectedItem: (state, action: PayloadAction<Entity | null>) => {
      state.selectedItem = action.payload;
    },
    updateFilter: (state, action: PayloadAction<Partial<FilterState>>) => {
      state.filters = { ...state.filters, ...action.payload };
    },
    clearError: (state) => {
      state.error = null;
    },
    resetState: () => initialState
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchEntities.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchEntities.fulfilled, (state, action) => {
        state.loading = false;
        state.items = action.payload;
      })
      .addCase(fetchEntities.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload as string;
      });
  }
});

export const { setSelectedItem, updateFilter, clearError, resetState } = entitySlice.actions;
export default entitySlice.reducer;

Async Thunk Patterns

CRUD Operations

// Create
export const createEntity = createAsyncThunk(
  'entities/createEntity',
  async (entityData: Omit<Entity, 'id'>, { rejectWithValue }) => {
    try {
      const response = await api.createEntity(entityData);
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response?.data?.message || 'Creation failed');
    }
  }
);

// Update
export const updateEntity = createAsyncThunk(
  'entities/updateEntity',
  async ({ id, updates }: { id: string; updates: Partial<Entity> }, { rejectWithValue }) => {
    try {
      const response = await api.updateEntity(id, updates);
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response?.data?.message || 'Update failed');
    }
  }
);

// Delete
export const deleteEntity = createAsyncThunk(
  'entities/deleteEntity',
  async (id: string, { rejectWithValue }) => {
    try {
      await api.deleteEntity(id);
      return id;
    } catch (error) {
      return rejectWithValue(error.response?.data?.message || 'Deletion failed');
    }
  }
);

Advanced State Patterns

Normalized State

import { createEntityAdapter, EntityState } from '@reduxjs/toolkit';

const entityAdapter = createEntityAdapter<Entity>({
  selectId: (entity) => entity.id,
  sortComparer: (a, b) => a.name.localeCompare(b.name)
});

interface ExtendedEntityState extends EntityState<Entity> {
  loading: boolean;
  error: string | null;
}

const initialState: ExtendedEntityState = entityAdapter.getInitialState({
  loading: false,
  error: null
});

// In extraReducers
.addCase(fetchEntities.fulfilled, (state, action) => {
  state.loading = false;
  entityAdapter.setAll(state, action.payload);
})
.addCase(updateEntity.fulfilled, (state, action) => {
  entityAdapter.updateOne(state, {
    id: action.payload.id,
    changes: action.payload
  });
})
.addCase(deleteEntity.fulfilled, (state, action) => {
  entityAdapter.removeOne(state, action.payload);
});

Selectors

import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from '../store';

// Basic selectors
export const selectEntitiesState = (state: RootState) => state.entities;
export const selectEntities = (state: RootState) => state.entities.items;
export const selectLoading = (state: RootState) => state.entities.loading;
export const selectError = (state: RootState) => state.entities.error;

// Memoized selectors
export const selectFilteredEntities = createSelector(
  [selectEntities, selectEntitiesState],
  (entities, entitiesState) => {
    const { search, category, sortBy } = entitiesState.filters;
    
    return entities
      .filter(entity => 
        entity.name.toLowerCase().includes(search.toLowerCase()) &&
        (category === 'all' || entity.category === category)
      )
      .sort((a, b) => {
        switch (sortBy) {
          case 'name': return a.name.localeCompare(b.name);
          case 'date': return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
          default: return 0;
        }
      });
  }
);

Best Practices

  • Type Safety: Always define interfaces for state, payloads, and API responses
  • Error Handling: Use rejectWithValue for consistent error handling in thunks
  • Loading States: Implement proper loading states for better UX
  • Immutability: Leverage Immer's draft state for clean mutations
  • Naming: Use descriptive names following the pattern domain/action
  • Initial State: Provide sensible defaults to prevent undefined states
  • Selectors: Create memoized selectors for computed values
  • Normalization: Use createEntityAdapter for collections of entities
  • Cleanup: Include reset/clear actions for state management

Configuration Tips

  • Group related slices in feature directories
  • Export both actions and selectors from slice files
  • Use consistent naming patterns across slices
  • Implement proper error boundaries in components
  • Consider using RTK Query for complex API interactions
  • Add middleware for logging in development
  • Use Redux DevTools for debugging state changes

FAQ

Common questions

Discussion

Questions & comments ยท 0

Sign In Sign in to leave a comment.