npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 --noEmit

10 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.