Skip to main content
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
setMaps
(maps: string[]) => void
Update selected maps filter
setTypes
(types: string[]) => void
Update selected objective types filter
setStatuses
(statuses: TaskStatus[]) => void
Update selected statuses filter
Update search query
setKappaOnly
(kappaOnly: boolean) => void
Toggle Kappa-only filter
resetFilters
() => void
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
toggleSummaryCompact
() => void
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

setSearch
(search: string) => void
Update search query
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)
setSortBy
(sortBy: SortBy) => void
Set sort field
toggleSortDir
() => void
Toggle between ascending and descending sort
setViewMode
(viewMode: ViewMode) => void
Switch between tier-grouped and grid view
resetFilters
() => void
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
addPin
(taskId: string) => void
Optimistically add a pin. Caller must persist to Dexie. Maximum 10 pins. Ignores duplicates.
removePin
(taskId: string) => void
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

setLevel
(level: number) => void
Update player level (clamped to 1-79)
setWipeId
(wipeId: string) => void
Update wipe ID
setAutoStart
(enabled: boolean) => void
Enable/disable auto-start for unlocked tasks
setLang
(lang: 'ja' | 'en') => void
Change UI language
setOnboardingDone
(done: boolean) => void
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
resetThresholds
() => void
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);

Build docs developers (and LLMs) love