Tarkov Kappa Navi uses Zustand for lightweight, hook-based state management. All stores are located in src/stores/.
Store Architecture
Stores manage ephemeral UI state and user preferences. They do not persist data directly—persistence is handled by Dexie (see Database Schema).
State vs. Persistence
- Stores: Reactive UI state, filters, selections, temporary settings
- Database: User progress, notes, pins, permanent settings
filterStore
Manages task filtering state for the Tasks page.
Location: src/stores/filterStore.ts
State Shape
interface FilterState {
traders: string[]; // Selected trader IDs
maps: string[]; // Selected map IDs
types: string[]; // Selected objective types
statuses: TaskStatus[]; // Selected statuses
search: string; // Search query
kappaOnly: boolean; // Show only Kappa-required tasks
}
Actions
setTraders
(traders: string[]) => void
Update selected traders filter
Update selected maps filter
setTypes
(types: string[]) => void
Update selected objective types filter
setStatuses
(statuses: TaskStatus[]) => void
Update selected statuses filter
setKappaOnly
(kappaOnly: boolean) => void
Toggle Kappa-only filter
Reset all filters to initial state
Usage
import { useFilterStore } from '@/stores/filterStore';
function TasksPage() {
const { traders, setTraders, resetFilters } = useFilterStore();
return (
<div>
<button onClick={() => setTraders(['prapor', 'therapist'])}>
Filter by Prapor & Therapist
</button>
<button onClick={resetFilters}>Clear Filters</button>
</div>
);
}
hideoutStore
Manages hideout UI state including station selection and summary view settings.
Location: src/stores/hideoutStore.ts
State Shape
interface HideoutState {
selectedStationId: string | null; // Currently selected station
mobileTab: 'list' | 'summary'; // Mobile view tab
summaryFilter: 'buildable' | 'not_built' | 'all'; // Summary filter
summaryCompact: boolean; // Compact summary view
}
Actions
setSelectedStationId
(stationId: string | null) => void
Set the currently selected hideout station
setMobileTab
(tab: 'list' | 'summary') => void
Switch between list and summary tabs on mobile
setSummaryFilter
(filter: 'buildable' | 'not_built' | 'all') => void
Filter summary view by build state
Toggle compact mode for summary view
Usage
import { useHideoutStore } from '@/stores/hideoutStore';
function HideoutPanel() {
const { selectedStationId, setSelectedStationId } = useHideoutStore();
return (
<div>
{selectedStationId ? (
<StationDetail stationId={selectedStationId} />
) : (
<p>Select a station</p>
)}
</div>
);
}
itemFilterStore
Manages item filtering and sorting for the Items page.
Location: src/stores/itemFilterStore.ts
State Shape
type SortBy = 'pricePerSlot' | 'bestSellPrice' | 'name';
type SortDir = 'asc' | 'desc';
type ViewMode = 'tier' | 'grid';
type TaskRelation = 'usedInKappaTask' | 'rewardFromTask' | 'collector';
interface ItemFilterState {
search: string;
types: string[]; // Item types (weapon, mods, medical, etc.)
tiers: PriceTier[]; // S, A, B, C, D
taskRelations: TaskRelation[];
sortBy: SortBy;
sortDir: SortDir;
viewMode: ViewMode;
}
Actions
setTypes
(types: string[]) => void
Update selected item types filter
setTiers
(tiers: PriceTier[]) => void
Update selected price tiers filter
setTaskRelations
(taskRelations: TaskRelation[]) => void
Filter by task relationship (used in Kappa tasks, task rewards, Collector)
Toggle between ascending and descending sort
setViewMode
(viewMode: ViewMode) => void
Switch between tier-grouped and grid view
Reset all filters to defaults
Usage
import { useItemFilterStore } from '@/stores/itemFilterStore';
function ItemsPage() {
const { tiers, setTiers, sortBy, setSortBy } = useItemFilterStore();
return (
<div>
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="pricePerSlot">Price per Slot</option>
<option value="bestSellPrice">Sell Price</option>
<option value="name">Name</option>
</select>
</div>
);
}
nowPinsStore
Manages the “Now” panel pinned tasks. State is optimistically updated and must be persisted to Dexie separately.
Location: src/stores/nowPinsStore.ts
State Shape
interface NowPinsState {
taskIds: string[]; // Max 10 pinned task IDs
}
Actions
hydrate
(taskIds: string[]) => void
Load initial state from database
Optimistically add a pin. Caller must persist to Dexie.
Maximum 10 pins. Ignores duplicates.
Optimistically remove a pin. Caller must persist to Dexie.
Usage
import { useNowPinsStore } from '@/stores/nowPinsStore';
import { db } from '@/db/database';
function NowPanel() {
const { taskIds, addPin, removePin } = useNowPinsStore();
const handleAddPin = async (taskId: string) => {
// Optimistic update
addPin(taskId);
// Persist to DB
const updatedIds = [...taskIds, taskId];
await db.nowPins.put({ id: 'me', taskIds: updatedIds });
};
return <div>{/* Render pins */}</div>;
}
Actions are optimistic only. You must persist changes to Dexie’s nowPins table yourself.
profileStore
Manages current player profile state including level, wipe ID, and settings.
Location: src/stores/profileStore.ts
State Shape
interface ProfileState {
currentLevel: number; // 1-79
wipeId: string; // Current wipe identifier
autoStartUnlocked: boolean; // Auto-start unlocked tasks
lang: 'ja' | 'en'; // UI language
onboardingDone: boolean; // Onboarding completed
}
Actions
Update player level (clamped to 1-79)
setAutoStart
(enabled: boolean) => void
Enable/disable auto-start for unlocked tasks
setLang
(lang: 'ja' | 'en') => void
Change UI language
Mark onboarding as complete
hydrate
(level: number, wipeId: string, autoStartUnlocked?: boolean, lang?: 'ja' | 'en', onboardingDone?: boolean) => void
Load profile from database
Usage
import { useProfileStore } from '@/stores/profileStore';
import { db } from '@/db/database';
function ProfileSettings() {
const { currentLevel, setLevel } = useProfileStore();
const handleLevelChange = async (newLevel: number) => {
setLevel(newLevel);
// Persist to DB
await db.profile.update('me', {
currentLevel: newLevel,
updatedAt: Date.now()
});
};
return <input type="number" value={currentLevel} onChange={e => handleLevelChange(+e.target.value)} />;
}
selectionStore
Manages the currently selected task for detail view.
Location: src/stores/selectionStore.ts
State Shape
interface SelectionState {
selectedTaskId: string | null;
}
Actions
setSelectedTaskId
(taskId: string | null) => void
Set the currently selected task ID, or null to deselect
Usage
import { useSelectionStore } from '@/stores/selectionStore';
function TaskList({ tasks }) {
const { selectedTaskId, setSelectedTaskId } = useSelectionStore();
return (
<div>
{tasks.map(task => (
<div
key={task.id}
className={selectedTaskId === task.id ? 'selected' : ''}
onClick={() => setSelectedTaskId(task.id)}
>
{task.name}
</div>
))}
</div>
);
}
tierStore
Manages customizable price tier thresholds. Persisted to localStorage via Zustand’s persist middleware.
Location: src/stores/tierStore.ts
State Shape
export interface TierThresholds {
S: number; // Default: 100,000
A: number; // Default: 50,000
B: number; // Default: 20,000
C: number; // Default: 10,000
// D tier is anything below C
}
interface TierState {
thresholds: TierThresholds;
}
Constants
export const DEFAULT_TIER_THRESHOLDS: TierThresholds = {
S: 100_000,
A: 50_000,
B: 20_000,
C: 10_000,
};
Actions
setThresholds
(thresholds: TierThresholds) => void
Update all tier thresholds
Reset to default thresholds
Hooks
// Get current thresholds
function useTierThresholds(): TierThresholds;
Usage
import { useTierStore, useTierThresholds, DEFAULT_TIER_THRESHOLDS } from '@/stores/tierStore';
function TierSettings() {
const { thresholds, setThresholds, resetThresholds } = useTierStore();
const currentThresholds = useTierThresholds();
return (
<div>
<label>
S Tier: ₽
<input
type="number"
value={thresholds.S}
onChange={e => setThresholds({ ...thresholds, S: +e.target.value })}
/>
</label>
<button onClick={resetThresholds}>Reset to Defaults</button>
</div>
);
}
This store uses Zustand’s persist middleware with storage key tarkov-kappa-tier-thresholds.
Best Practices
Hydration Pattern
Load database state into stores on app initialization:
import { db } from '@/db/database';
import { useProfileStore } from '@/stores/profileStore';
import { useNowPinsStore } from '@/stores/nowPinsStore';
async function initializeApp() {
// Load profile
const profile = await db.profile.get('me');
if (profile) {
useProfileStore.getState().hydrate(
profile.currentLevel,
profile.wipeId,
profile.autoStartUnlocked,
profile.lang,
profile.onboardingDone
);
}
// Load now pins
const pins = await db.nowPins.get('me');
if (pins) {
useNowPinsStore.getState().hydrate(pins.taskIds);
}
}
Optimistic Updates
For better UX, update store state immediately and persist asynchronously:
const handleStatusChange = async (taskId: string, newStatus: TaskStatus) => {
// 1. Update UI immediately (via React Query cache mutation)
queryClient.setQueryData(['progress'], old => {
// Update progress optimistically
});
// 2. Persist to database
await db.progress.put({
taskId,
status: newStatus,
completedAt: newStatus === 'done' ? Date.now() : null,
updatedAt: Date.now()
});
};
Selector Pattern
Use selectors to subscribe to specific state slices:
// Bad: Re-renders on any filter change
const filters = useFilterStore();
// Good: Only re-renders when search changes
const search = useFilterStore(state => state.search);
const setSearch = useFilterStore(state => state.setSearch);