Skip to main content

Redux State

Redux Toolkit Setup

State management uses Redux Toolkit with typed hooks.

Typed Hooks

Always use the typed hooks instead of raw useDispatch/useSelector:

// Correct
import { useAppDispatch, useAppSelector } from '@/store';

// Incorrect
import { useDispatch, useSelector } from 'react-redux';

Slices

Each module has its own Redux slice:

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

export const fetchTests = createAsyncThunk('browser/fetchTests', async (_, { getState }) => {
// API call
});

const browserSlice = createSlice({
name: 'browser',
initialState,
reducers: { /* sync reducers */ },
extraReducers: (builder) => {
builder.addCase(fetchTests.fulfilled, (state, action) => {
state.tests = action.payload;
});
},
});

State Management Architecture

The state management layer spans Redux (global async state), React hooks (local/domain state), and browser APIs (persistence). Here is how they connect:

Where State Lives

State categoryMechanismPersistence
Agent list, tasks, metricsRedux (agentSlice)None (refetched)
Analytics filtersuseAnalyticsFilters hook + URL paramsURL (shareable)
Test favorites & recentsuseTestPreferences hooklocalStorage (per-user key)
Theme preferenceuseTheme hooklocalStorage
Sidebar collapseSidebarLayoutlocalStorage
Auth tokensClerk SDKClerk session
Analytics configAnalyticsAuthProvider contextBackend settings
Feature flagsuseCapabilities hookNone (fetched on mount)

Agent Slice (Deep Dive)

The agentSlice is the primary Redux slice, managing distributed agent operations.

State Shape

interface AgentState {
agents: AgentSummary[];
selectedAgent: Agent | null;
tasks: AgentTask[];
metrics: AgentMetrics | null;
loading: boolean;
error: string | null;
filters: ListAgentsRequest;
pagination: PaginationState;
}

Async Thunks

ThunkPurpose
fetchAgentsLoad agent list with filtering and pagination
fetchAgentLoad detailed agent information
fetchTasksLoad task history
createTasksDispatch new tasks to agents
updateAgentStatusChange agent operational status
tagAgent / untagAgentManage agent categorization tags

Usage Pattern

const dispatch = useAppDispatch();
const { agents, loading, error } = useAppSelector(state => state.agent);

// Load agents with filters
dispatch(fetchAgents({ status: 'active', limit: 50 }));

// Create tasks for multiple agents
dispatch(createTasks({
agentIds: selectedAgentIds,
testUuids: selectedTests,
priority: 'high'
}));

Error Handling in Thunks

All async thunks use rejectWithValue for consistent error propagation:

try {
return await agentApi.listAgents(filters);
} catch (error: unknown) {
return rejectWithValue(extractErrorMessage(error, 'Failed to fetch agents'));
}

Components read errors from the slice:

const { error } = useAppSelector(state => state.agent);
if (error) {
return <Alert variant="destructive">{error}</Alert>;
}

Custom Hooks Catalog

Beyond Redux, several standalone hooks manage domain-specific state.

useAnalyticsFilters

Manages 11+ filter types for analytics dashboards with bidirectional URL synchronization.

Filter categories:

  • Basic -- Organization, date range, result status
  • Advanced -- Hostnames, tests, techniques, categories, severities, threat actors, tags, errors, bundles
const {
filters,
isExpanded,
hasActiveFilters,
setDateRange,
setResult,
clearAllFilters,
getApiParams
} = useAnalyticsFilters();

// Convert to API parameters
const apiParams = getApiParams();
const data = await analyticsApi.getExecutions(apiParams);
tip

Filters automatically sync with URL parameters while preserving other query params (like active tabs). This enables shareable dashboard URLs and browser back/forward navigation.

Date range windowing -- The getWindowDaysForDateRange utility maps ranges to rolling window sizes:

Selected rangeWindow size
7 days or less7-day (full coverage)
8--30 days7-day rolling
31--90 days30-day rolling
CustomCalculated from actual span

useTestPreferences

Manages user-specific favorites and recently-viewed tests. Data is stored in localStorage with user-specific keys.

const { favorites, recentTests, isFavorite, toggleFavorite, trackView } = useTestPreferences();

// Track when user views a test
trackView(testUuid, testName);

// Check/toggle favorite status
if (isFavorite(testUuid)) {
// Show filled star
}

Cross-tab sync -- Uses custom events to synchronize preference changes across browser tabs in real-time. The recent list auto-maintains the 20 most recently viewed tests.

useTheme

Multi-theme system with style variants:

const { theme, themeStyle, phosphorVariant, setTheme, setThemeStyle, toggleThemeStyle } = useTheme();
PropertyTypeValues
themeBase'light' | 'dark'
themeStyleVariant'default' | 'neobrutalism' | 'hackerterminal'
phosphorVariantTerminal color'green' | 'amber'

The hook manages CSS classes on <html>: .dark/.light for base themes, .neobrutalism/.hackerterminal for variants, .phosphor-amber for terminal color.

useCapabilities

Detects available features based on deployment environment (Docker vs Railway vs Vercel, etc.):

const { build, buildUpload, certGenerate, gitSync, platform } = useCapabilities();

if (build) {
// Show build button
}
info

Defaults to all features available (Docker-style deployment). Gracefully handles backends that don't expose a /api/capabilities endpoint.

useDefenderConfig

Checks if Microsoft Defender integration is configured, for conditional UI rendering:

const { configured, loading } = useDefenderConfig();

URL State Synchronization Pattern

The analytics filters demonstrate the pattern for syncing complex state with URL parameters:

// Preserve non-filter params while updating filters
setSearchParams(prev => {
const newParams = new URLSearchParams(prev);
// Clear filter params, then add current values
filterKeys.forEach(key => newParams.delete(key));
Object.entries(filterParams).forEach(([key, value]) => {
newParams.set(key, value);
});
return newParams;
}, { replace: true });

This pattern enables shareable URLs while maintaining other application state like active tabs or modal states.

Provider Setup

Several hooks require provider components in the app root. The order matters:

function App() {
return (
<ClerkProvider>
<ThemeProvider>
<AnalyticsAuthProvider>
<Provider store={store}>
<AppContent />
</Provider>
</AnalyticsAuthProvider>
</ThemeProvider>
</ClerkProvider>
);
}
warning

useAuthenticatedApi must run inside ClerkProvider. useAnalyticsAuth must run inside AnalyticsAuthProvider. Placing hooks outside their required provider will throw a runtime error or return undefined.