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​

Compatibility shim for the single f0 dark theme:

const { theme } = useTheme(); // always 'dark'

The provider API is kept so existing consumers and tests keep compiling, but theme is pinned to 'dark', the style/phosphor variants are gone, and nothing is written to localStorage. <html> carries class="dark" permanently.

Prefer reading design tokens (bg-surface, text-faint, var(--accent)) over branching on theme in new code.

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.