@startsimpli/hooks
v0.4.20
Published
Shared React hooks for StartSimpli apps
Downloads
1,201
Readme
@startsimpli/hooks
Shared React hooks for every StartSimpli app. This is where dashboard plumbing lives — entity tables with CSV export, paginated table filters, saved views, recently-viewed, wizards, TanStack Query wrappers over @startsimpli/api (messages, target lists, vault), enrichment state machines, and small a11y/lifecycle helpers (useReducedMotion, useRefetchOnFocus). Reach for this package whenever you're building a list/detail/edit screen, a multi-step form, or anything that talks to the Django backend through @startsimpli/api.
Consumed today by raise-simpli/web-app, market-simpli, vault-web, and the React Native preview in examples/mobile-rn. Web-only hooks have .native.ts siblings so React Native bundlers resolve a safe stub (or platform-appropriate implementation) automatically.
Install
Workspace dep — in the app's package.json:
{ "dependencies": { "@startsimpli/hooks": "workspace:*" } }Add transpilePackages: ['@startsimpli/hooks'] to next.config.ts (Next apps only). Peer deps are react, @tanstack/react-query (for any hook that talks to the API), and @startsimpli/api (for the typed-API hooks); all are marked optional so a leaf app that only wants useReducedMotion doesn't have to install the world.
Public surface
Data + mutations (TanStack Query over @startsimpli/api)
| Export | Signature | Consumed by |
|---|---|---|
| useCRUDMutation | (mutationFn, { invalidateKeys, onSuccess?, onError? }) => UseMutationResult — generic mutation that invalidates a list of query keys on success. | available; no app consumers yet |
| useMessages | (api: MessagesApi, filters?) => UseQueryResult<PaginatedResponse<Message>> — paginated message list. | market-simpli, raise-simpli |
| useMessage | (api, id) => UseQueryResult<Message> — single message detail. | market-simpli |
| useCreateMessage / useSendMessage / useScheduleMessage / useSendTestMessage | message lifecycle mutations; invalidate the messages cache. | market-simpli |
| useMessageChannels | (api) => UseQueryResult<Channel[]> — available send channels. | market-simpli |
| useMessageTemplates / useMessageTemplate / useCreateMessageTemplate / useUpdateMessageTemplate / useDeleteMessageTemplate | template CRUD over MessageTemplatesApi. | market-simpli |
| useTargetListDetail | (id, { get }) => UseQueryResult<TargetList> — fetch one list. | market-simpli |
| useTargetListMutations | ({ apiFns, onSuccess?, onError? }) => { createList, updateList, deleteList, addMembers, removeMembers, refreshList } — bundled list/member mutations with shared invalidation. | market-simpli |
| TARGET_LIST_KEYS | query-key factory { all, lists(filters), detail(id), members(listId, filters) }. | market-simpli |
Vault (startsim-d30.3.2) — over VaultApi
| Export | Signature | Consumed by |
|---|---|---|
| useEnvironments / useEnvironment | list and detail queries for vault environments. | vault-web |
| useCreateEnvironment / useUpdateEnvironment / useDeleteEnvironment | environment mutations; invalidate the environments cache. | vault-web |
| useSecrets / useCreateSecret / useUpdateSecret / useDeleteSecret | per-environment secret CRUD; also refresh the env detail/list so secret_count stays accurate. | vault-web |
| useRevealSecret | on-demand mutation — values are never auto-fetched. | vault-web |
| useAccessKeys / useCreateAccessKey / useDeleteAccessKey | access-key CRUD per environment. | vault-web |
| useAuditLog | paginated audit log for an environment. | vault-web |
Tables, filters, saved views
| Export | Signature | Consumed by |
|---|---|---|
| useEntityTable<T> | ({ items, idField?, csvFilename, csvColumns, csvRowMapper }) => { selectedItem, editingItem, viewMode, showCreateForm, exportIdsRef, exportCSV, isExporting, openView, openEdit, openCreate, closePanel } — list/detail/edit + CSV export state for an entity grid. | market-simpli (prospects, organizations, deals, lists, …) |
| useTableFilters<TFilters> | (initial) => { filters, setFilter, setPage, setPageSize, setSearch, setSort, resetFilters } — generic paginated table filter state; mutating a filter resets page to 1. | market-simpli (wrapped as useMarketTableFilters) |
| useSavedViews<T> | ({ resource, loadFn, saveFn, updateFn, deleteFn }) => { views, currentViewId, loading, error, saveView, updateView, deleteView, loadView, getCurrentView, refreshViews } — persisted named filter views. | market-simpli (SavedViewsMenu) |
| useRecentlyViewed<T> | (storageKey, maxItems=5) => { items, timestamps, trackView, clear } — localStorage-backed recents list. | raise-simpli (RecentlyViewedSidebar) |
URL-encoded filter state (pure functions, no React)
| Export | Purpose |
|---|---|
| encodeFilterConfig / decodeFilterConfig | URL-safe base64 (de)serializer for a FilterConfig. |
| parseUrlFilters | URLSearchParams -> EncodedFilterState. |
| createSimpleFilter / mergeFilters | helpers for constructing/combining filter graphs. |
| getFilterDescription | human-readable summary for a saved filter chip. |
| FilterOperator, FilterValue, FilterCondition, FilterGroup, FilterConfig, EncodedFilterState, FilterValidationError, FilterValidationResult | types. |
Used by raise-simpli/web-app/src/lib/filtering/parser.ts (which re-exports them for backwards-compat).
Wizards + forms
| Export | Signature | Consumed by |
|---|---|---|
| useWizard<TStep> | (steps, initialStep?, opts?) or (steps, opts?) → { currentStep, stepIndex, totalSteps, isFirstStep, isLastStep, canGoBack, canGoNext, errors, goTo, next, prev, reset, clearErrors } — typed step machine with per-step validators. | market-simpli (CampaignForm) |
| useAsyncOptions<T> | (fetcher, { enabled?, defaultValue? }?) => { data, loading, error, refresh } — load select options from an async source. | market-simpli (CampaignForm) |
CSV import / export
| Export | Signature | Consumed by |
|---|---|---|
| useCSVImport<TField> | ({ previewFn, importFn, onSuccess? }) => UseCSVImportState — platform-neutral upload → preview → mapping → import state machine; safe on web and React Native. | market-simpli (ImportCSV) |
| useCSVExport | ({ exportFn, filename? }) => { exportCSV, isExporting } — web-only download via Blob + anchor; React Native resolves a stub that throws. | market-simpli (also driven by useEntityTable) |
Enrichment
| Export | Purpose | Consumed by |
|---|---|---|
| useEnrichment | single-contact enrichment state. | market-simpli (settings/enrichment page) |
| useBatchEnrichment | batched enrichment with progress + cancel. | market-simpli |
| useQueueStatus | poll the enrichment queue depth. | market-simpli |
Lifecycle + a11y
| Export | Signature | Consumed by |
|---|---|---|
| useReducedMotion | () => boolean — tracks (prefers-reduced-motion: reduce). | raise-simpli (toast-notification, InvestorCard, PipelineColumn), examples/mobile-rn |
| useRefetchOnFocus | (refetch, pathname, enabled=true) => void — re-runs refetch on tab-focus and route change, skipping initial mount, debounced 2s. | raise-simpli (dashboard, messages, fundraises, calendar, outreach pages) |
Usage
Entity table + CSV export (market-simpli)
// market-simpli/src/modules/prospects/components/ProspectsTable.tsx
import { useEntityTable } from '@startsimpli/hooks'
const {
selectedItem, editingItem, viewMode, showCreateForm,
openView, openEdit, openCreate, closePanel,
} = useEntityTable<Prospect>({
items: data?.results || [],
idField: 'internalId',
csvFilename: 'prospects',
csvColumns: ['Name', 'Email', 'Phone', 'Company', 'Stage', 'Lead Score', 'Created'],
csvRowMapper: (p) => [
p.name || '',
p.email || '',
p.phone || '',
p.companyName || '',
p.stage || '',
p.leadScore?.toString() || '',
new Date(p.createdAt).toLocaleDateString(),
],
})Vault environments (vault-web)
// vault-web/src/app/(dashboard)/environments/page.tsx
import {
useCreateEnvironment,
useDeleteEnvironment,
useEnvironments,
useUpdateEnvironment,
} from '@startsimpli/hooks'
import { api } from '@/lib/api'
const { data, isLoading, isError } = useEnvironments(api.vault)
const createEnv = useCreateEnvironment(api.vault)
const updateEnv = useUpdateEnvironment(api.vault)
const deleteEnv = useDeleteEnvironment(api.vault)Secret CRUD takes the env slug once and returns mutations scoped to it; deleting/creating a secret automatically refreshes the env detail + the environments list so secret_count stays accurate:
// vault-web/src/app/(dashboard)/environments/[slug]/page.tsx
import {
useCreateSecret,
useDeleteSecret,
useRevealSecret,
useSecrets,
useUpdateSecret,
} from '@startsimpli/hooks'
const { data, isLoading } = useSecrets(api.vault, slug)
const createSecret = useCreateSecret(api.vault, slug)
const updateSecret = useUpdateSecret(api.vault, slug)
const deleteSecret = useDeleteSecret(api.vault, slug)
const reveal = useRevealSecret(api.vault, slug)Wizard with per-step validation (market-simpli)
// market-simpli/src/modules/campaigns/components/CampaignForm.tsx
import { useWizard } from '@startsimpli/hooks'
const STEPS = ['details', 'sender', 'sequence'] as const
type CampaignStep = (typeof STEPS)[number]
const wizard = useWizard<CampaignStep>(STEPS, {
validate: {
details: () => {
const errs: Record<string, string> = {}
if (!formDataRef.current.name.trim()) errs.name = 'Campaign name is required'
if (!formDataRef.current.channel) errs.channel = 'Please select a messaging channel'
return Object.keys(errs).length ? errs : null
},
sequence: () => {
const errs: Record<string, string> = {}
if (formDataRef.current.sequence.length === 0) {
errs.sequence = 'Please add at least one step to the campaign sequence'
}
return Object.keys(errs).length ? errs : null
},
},
})Refetch on tab focus / route change (raise-simpli)
// raise-simpli/web-app/src/app/(dashboard)/messages/page.tsx
import { useRefetchOnFocus } from '@startsimpli/hooks'
import { usePathname } from 'next/navigation'
useRefetchOnFocus(fetchMessages, usePathname() ?? '')Saved views (market-simpli)
// market-simpli/src/shared/components/SavedViewsMenu.tsx
import { useSavedViews } from '@startsimpli/hooks'
const {
views, currentViewId, loading,
saveView: saveNewView, deleteView: removeView, loadView,
} = useSavedViews<MarketSavedView>({
resource,
loadFn: loadViews,
saveFn: saveView,
updateFn: updateView,
deleteFn: deleteView,
})Verification
cd packages/hooks
pnpm vitest run
pnpm tsc --noEmit10 test files, 83 tests passing (useAsyncOptions, useCRUDMutation, useEntityTable, useRecentlyViewed, useReducedMotion, useRefetchOnFocus, useSavedViews, useTableFilters, useVault, useWizard).
Shared-first
See monorepo CLAUDE.md rule 9: hooks live here, not in any app's src/. If a hook is used by one app today but plausibly useful to another (a paginated table filter, a CSV importer, a saved-view menu, a wizard, a TanStack Query wrapper over @startsimpli/api), write it in this package from day one. Don't wrap a shared hook in an app-local context just to add a field — extend the shared signature.
