Files
snippet-pastebin/docs/REDUX-GUIDE.md
2026-01-17 21:28:10 +00:00

9.8 KiB

Redux Implementation Guide

This document explains the Redux state management architecture implemented in the CodeSnippet application.

Overview

The application uses Redux Toolkit (RTK) for centralized state management. Redux provides a predictable state container that makes the application easier to understand, debug, and test.

Store Structure

src/store/
├── index.ts              # Store configuration
├── hooks.ts              # Typed Redux hooks
├── selectors.ts          # Memoized selectors
└── slices/
    ├── snippetsSlice.ts  # Snippet state & actions
    ├── namespacesSlice.ts # Namespace state & actions
    └── uiSlice.ts        # UI state & actions

State Slices

1. Snippets Slice

Location: src/store/slices/snippetsSlice.ts

State:

{
  items: Snippet[]           // All snippets
  loading: boolean           // Loading state for async operations
  error: string | null       // Error message if operations fail
  selectedIds: Set<string>   // IDs of selected snippets (for bulk operations)
  selectionMode: boolean     // Whether selection mode is active
}

Async Actions (Thunks):

  • fetchAllSnippets() - Load all snippets from database
  • fetchSnippetsByNamespace(namespaceId) - Load snippets for specific namespace
  • createSnippet(snippetData) - Create new snippet
  • updateSnippet(snippet) - Update existing snippet
  • deleteSnippet(id) - Delete snippet by ID
  • bulkMoveSnippets({ snippetIds, targetNamespaceId }) - Move multiple snippets to namespace

Sync Actions:

  • toggleSelectionMode() - Enable/disable selection mode
  • toggleSnippetSelection(id) - Toggle selection of specific snippet
  • clearSelection() - Clear all selections
  • selectAllSnippets() - Select all current snippets

2. Namespaces Slice

Location: src/store/slices/namespacesSlice.ts

State:

{
  items: Namespace[]        // All namespaces
  selectedId: string | null // Currently selected namespace ID
  loading: boolean          // Loading state
  error: string | null      // Error message
}

Async Actions:

  • fetchNamespaces() - Load all namespaces and ensure default exists
  • createNamespace(name) - Create new namespace
  • deleteNamespace(id) - Delete namespace (moves snippets to default)

Sync Actions:

  • setSelectedNamespace(id) - Switch to different namespace

3. UI Slice

Location: src/store/slices/uiSlice.ts

State:

{
  dialogOpen: boolean          // Snippet editor dialog open/closed
  viewerOpen: boolean          // Snippet viewer dialog open/closed
  editingSnippet: Snippet | null // Snippet being edited (null for new)
  viewingSnippet: Snippet | null // Snippet being viewed
  searchQuery: string          // Current search text
}

Sync Actions:

  • openDialog(snippet) - Open editor dialog (snippet = null for new, snippet = existing for edit)
  • closeDialog() - Close editor dialog
  • openViewer(snippet) - Open viewer dialog with snippet
  • closeViewer() - Close viewer dialog
  • setSearchQuery(query) - Update search query

Selectors

Location: src/store/selectors.ts

Selectors provide optimized access to state with memoization to prevent unnecessary re-renders.

Basic Selectors:

selectSnippets(state)              // Get all snippets
selectSnippetsLoading(state)       // Get loading state
selectSelectionMode(state)         // Get selection mode
selectSelectedIds(state)           // Get selected snippet IDs
selectNamespaces(state)            // Get all namespaces
selectSelectedNamespaceId(state)   // Get selected namespace ID
selectSearchQuery(state)           // Get search query
selectDialogOpen(state)            // Get dialog open state
selectViewerOpen(state)            // Get viewer open state
selectEditingSnippet(state)        // Get editing snippet
selectViewingSnippet(state)        // Get viewing snippet

Computed Selectors (Memoized):

selectSelectedNamespace(state)     // Get full selected namespace object
selectFilteredSnippets(state)      // Get snippets filtered by search query
selectSelectedSnippets(state)      // Get full snippet objects for selected IDs

Custom Hooks

Location: src/store/hooks.ts

Type-safe hooks for accessing Redux:

import { useAppDispatch, useAppSelector } from '@/store/hooks'

// In components:
const dispatch = useAppDispatch()          // Typed dispatch
const snippets = useAppSelector(selectSnippets) // Typed selector

Usage Example

import { useEffect } from 'react'
import { useAppDispatch, useAppSelector } from '@/store/hooks'
import {
  fetchSnippetsByNamespace,
  createSnippet,
  deleteSnippet,
} from '@/store/slices/snippetsSlice'
import {
  selectFilteredSnippets,
  selectSnippetsLoading,
  selectSearchQuery,
} from '@/store/selectors'
import { setSearchQuery } from '@/store/slices/uiSlice'

function MyComponent() {
  const dispatch = useAppDispatch()
  
  // Select state
  const snippets = useAppSelector(selectFilteredSnippets)
  const loading = useAppSelector(selectSnippetsLoading)
  const searchQuery = useAppSelector(selectSearchQuery)
  
  // Load snippets on mount
  useEffect(() => {
    dispatch(fetchSnippetsByNamespace('default-namespace-id'))
  }, [dispatch])
  
  // Handle search
  const handleSearch = (query: string) => {
    dispatch(setSearchQuery(query))
  }
  
  // Create snippet
  const handleCreate = async () => {
    try {
      await dispatch(createSnippet({
        title: 'New Snippet',
        description: 'Description',
        code: 'console.log("Hello")',
        language: 'javascript',
        category: 'JavaScript',
      })).unwrap()
      
      console.log('Snippet created successfully')
    } catch (error) {
      console.error('Failed to create snippet:', error)
    }
  }
  
  // Delete snippet
  const handleDelete = async (id: string) => {
    try {
      await dispatch(deleteSnippet(id)).unwrap()
      console.log('Snippet deleted')
    } catch (error) {
      console.error('Failed to delete:', error)
    }
  }
  
  if (loading) return <div>Loading...</div>
  
  return (
    <div>
      <input
        value={searchQuery}
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="Search snippets..."
      />
      
      <button onClick={handleCreate}>Create Snippet</button>
      
      {snippets.map(snippet => (
        <div key={snippet.id}>
          <h3>{snippet.title}</h3>
          <button onClick={() => handleDelete(snippet.id)}>Delete</button>
        </div>
      ))}
    </div>
  )
}

Benefits

1. Predictable State Updates

  • All state changes go through reducers
  • Same input always produces same output
  • Easy to trace how state changes over time

2. Debugging

  • Redux DevTools extension shows:
    • Every action dispatched
    • State before and after each action
    • Time-travel debugging (undo/redo actions)
    • Action history and diffs

3. Testing

import { configureStore } from '@reduxjs/toolkit'
import snippetsReducer, { createSnippet } from './slices/snippetsSlice'

describe('Snippets Slice', () => {
  it('should handle createSnippet', async () => {
    const store = configureStore({
      reducer: { snippets: snippetsReducer }
    })
    
    await store.dispatch(createSnippet({
      title: 'Test',
      // ... other fields
    }))
    
    const state = store.getState()
    expect(state.snippets.items).toHaveLength(1)
    expect(state.snippets.items[0].title).toBe('Test')
  })
})

4. Performance

  • Memoized selectors prevent unnecessary re-renders
  • Only components using changed state re-render
  • Efficient state updates with Immer

5. Type Safety

  • Full TypeScript integration
  • IntelliSense for all actions and state
  • Compile-time error catching

Migration from Component State

Before (Component State):

function SnippetManager() {
  const [snippets, setSnippets] = useState<Snippet[]>([])
  const [loading, setLoading] = useState(false)
  
  const loadSnippets = async () => {
    setLoading(true)
    const data = await getAllSnippets()
    setSnippets(data)
    setLoading(false)
  }
  
  // Pass down through props...
}

After (Redux):

function SnippetManager() {
  const dispatch = useAppDispatch()
  const snippets = useAppSelector(selectSnippets)
  const loading = useAppSelector(selectSnippetsLoading)
  
  useEffect(() => {
    dispatch(fetchAllSnippets())
  }, [dispatch])
  
  // State available anywhere via hooks - no prop drilling
}

Best Practices

  1. Use Async Thunks for Side Effects

    • Database calls
    • API requests
    • Any async operations
  2. Keep Slices Focused

    • Each slice manages related state
    • Don't create mega-slices
  3. Use Selectors

    • Don't access state directly
    • Use memoized selectors for computed values
    • Prevents unnecessary re-renders
  4. Handle Loading States

    • Show loading indicators during async operations
    • Handle errors gracefully
    • Use pending/fulfilled/rejected cases
  5. Type Everything

    • Define state interfaces
    • Type action payloads
    • Use typed hooks

Redux DevTools

Install the Redux DevTools Extension:

Features:

  • Inspect every action and state change
  • Time-travel debugging
  • Action replay
  • State diff viewer
  • Export/import state

Further Reading