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

@magic_npm/simple-tools

v1.4.0

Published

`simple-tools` is a simple set of tools

Readme

simple-tools

A utility toolkit with useData and useApi for improving code quality and development efficiency

中文文档

Installation

npm

npm i @magic_npm/simple-tools

yarn

yarn add @magic_npm/simple-tools

What Problems Does It Solve

useData — Object Declaration & Reset

In frontend development, you frequently need to reset data: clearing forms after submission, restoring default filter values when switching tabs, reverting state when closing dialogs. Manually assigning fields one by one is error-prone and verbose. useData lets you declare a data structure once, then reset or partially merge at any time.

Without useData:

// Resetting a form field by field — easy to miss one
form.name = ''
form.mobile = ''
form.remember = false
// Adding a new field but forgetting to reset it? That's a bug

With useData:

const [form, formOperate] = useData(() => ({
  name: '',
  mobile: '',
  remember: false
}))

formOperate.merge({ name: 'test' })  // Partial update
formOperate.reset()                   // One-click reset, nothing missed
formOperate.reset({ name: 'new' })    // Reset then fill new data (edit scenario)

Typical scenarios:

  • Form data reset (clear after submission, switch edit target)
  • Filter condition reset (restore defaults when switching tabs)
  • Dialog/drawer state cleanup (clear temporary data on close)
  • Multiple forms coexisting (declare multiple useData in one component with custom naming)

useApi — API Request Standardization

Frontend API calls involve boilerplate code: loading state management, duplicate submission prevention, parameter preprocessing, response data transformation, error handling. These are scattered across components with inconsistent patterns and easy to miss. useApi consolidates these into a declarative configuration, keeping component code focused on business logic.

Without useApi:

const loading = ref(false)
const getData = async () => {
  if (loading.value) return           // Prevent duplicate
  loading.value = true
  try {
    const res = await fetchList(query)
    const data = res.data              // Every component handles res.data
    list.value = data.list
  } catch (e) {
    console.error(e)                   // Inconsistent error handling
  } finally {
    loading.value = false
  }
}

With useApi:

const [getData, api] = useApi(fetchList, {
  loading: ref(false),
  params: () => query,
  allowRepeat: false,
  formatResponse: (res) => res.data.list,
  onSuccess: (data) => {
    list.value = data
  }
})

getData()        // Call
api.loading      // Loading state
api.status       // 'loading' | 'success' | 'error'

Typical scenarios:

  • List queries — Automatic loading, parameter merging, response transformation
  • Prevent duplicate submissions — allowRepeat: false blocks consecutive clicks during form submission
  • Cancel previous requests — Search suggestion scenarios, cancelPrevious: true keeps only the latest result
  • Load once — Config data that doesn't change, singleton: true caches the first result
  • One API, multiple purposes — Same request for load, load more, refresh, differentiated by loadingKey
  • Pre-request validation — before hook to block requests when validation fails
  • Data transformation isolation — formatResponse handles field mapping, status code conversion, time formatting — components receive ready-to-use data

useData + useApi Combined

The most common pattern in real development: useData declares parameters and data, useApi manages request flow. Together they cover the full "declare params → make request → transform response → update data" chain.

// 1. Declare request parameters
const [query, queryOperate] = useData(() => reactive({
  keyword: '',
  status: 'all',
  page: 1
}))

// 2. Declare response data
const [listData, listOperate] = useData(() => reactive({
  list: [],
  total: 0
}))

// 3. Declare API call
const [getList, api] = useApi(fetchList, {
  loading: ref(false),
  params: () => query,
  formatResponse: (res) => ({
    list: res.data.list,
    total: res.data.total,
    statusText: statusMap[res.data.status]
  }),
  onSuccess: (data) => {
    listOperate.merge(data)
  }
})

// 4. Business triggers
const onSearch = () => {
  queryOperate.merge({ page: 1 })
  getList()
}

const onReset = () => {
  queryOperate.reset()
  getList()
}

useData

Declare data models, merge and reset data. Common scenarios: resetting filter conditions, restoring form data.

Basic Usage

import { useData } from '@magic_npm/simple-tools'

const [form, formOperate] = useData(() => ({
  name: '',
  mobile: ''
}))

formOperate.merge({ name: 'test' })   // Merge data (shallow, only existing keys)
formOperate.reset()                    // Reset to initial values
formOperate.reset({ name: 'new' })     // Reset then merge

Vue Reactive Data

import { reactive } from 'vue'

const [query, queryOperate] = useData(() => reactive({
  id: '',
  name: ''
}))

queryOperate.merge({ id: '1' })
query.id  // '1' — page updates automatically

Multiple Declarations in One Component

const [searchForm, searchOperate] = useData(() => ({ keyword: '' }))
const [editForm, editOperate] = useData(() => ({ name: '', age: 0 }))

searchOperate.merge({ keyword: 'test' })
editOperate.reset()

API

Parameters

| Parameter | Return | | - | :-: | | useData(func: () => object) | [data model, operate object] |

Operate Methods

| Method | Parameter | Return | Description | | - | - | :-: | - | | operate.merge(object) | object | object | Shallow merge onto model, only existing keys | | operate.reset(object?) | object? | object | Reset to initial values, optionally merge after |

Standalone Utilities

import { getDataType, mergeData } from '@magic_npm/simple-tools'

getDataType(data)           // Returns 'null' | 'undefined' | 'object' | 'array' | 'number' | 'string' | 'set'
mergeData(object1, object2) // Shallow merge object2 into object1 (only existing keys)

useApi

API request manager for managing request lifecycle, data transformation, loading state, and request strategies.

Data Flow

params() → override merge → before intercept → formatRequest transform → api() call → formatResponse transform → onSuccess(data, { res, loadingKey, params }) callback
                   ↑                                                                                              ↘ onError callback (any stage failure)
                   └── rawParams (pre-formatRequest params, passed to onSuccess context.params) ──────────────────┘

Parameter Isolation

Parameters are deep-cloned (via structuredClone) at two points to ensure data safety:

  1. External isolation — After merging params() with override, the result is cloned as paramsSnapshot. This decouples from the external object so that api() and its consumers never mutate the original params source.
  2. formatRequest isolation — Before passing to formatRequest, paramsSnapshot is cloned again. This ensures formatRequest's output does not alter paramsSnapshot, which is also used by before, onLoadingStart, and onSuccess(context.params).

Three-layer isolation: external object → paramsSnapshot → formatRequest output → api() call.

This also means special types like File, Blob, Date, Map, Set are preserved through the entire pipeline.

Type Inference

Type inference in useApi flows from the api function, not from params. The api function is an external contract with fixed parameter and return types — all other type parameters are derived from it.

Inference chain (no formatRequest):

api: (arg: Q) => Promise<R>
  ├─ Q  ← api arg type, constrains params: () => Q
  └─ R  ← api return type, constrains formatResponse: (res: R) => RD
function httpA(a: { id: string }) { ... }

useApi(httpA, {
  params: () => ({ id: '1', extra: 'ok' }),  // ✅ extra fields OK
  params: () => ({ id: 1 }),                   // ❌ id type mismatch
  params: () => ({}),                          // ❌ missing required field
  formatResponse: (res) => res.data,           // res inferred as R
  onSuccess: (data) => { ... }                 // data inferred as RD
})

Inference chain (with formatRequest):

api: (arg: OQ) => Promise<R>
  ├─ OQ ← api arg type, constrains formatRequest return type
  ├─ Q  ← formatRequest: (params: Q) => OQ, constrains params: () => Q
  └─ R  ← api return type
function httpB(a: { id: string; token: string }) { ... }

useApi(httpB, {
  params: () => ({ id: '1' }),
  formatRequest: (q) => ({ ...q, token: getToken() }),  // Q → OQ bridge
  formatResponse: (res) => res.data,
  onSuccess: (data) => { ... }
})

Key rules:

  • Can add extra fields — params can return a superset of what api requires (structural subtyping)
  • Cannot omit fields — params must satisfy all required fields of api's parameter type
  • Cannot mismatch types — field types must be compatible
  • formatResponse is required for typed onSuccess — without formatResponse, onSuccess(data) will be unknown; provide formatResponse to infer RD

Basic Usage

import { reactive, ref } from 'vue'
import { useData, useApi } from '@magic_npm/simple-tools'

const [query, queryOperate] = useData(() => reactive({ id: '', name: '' }))
const [data, dataOperate] = useData(() => reactive({ name: '', mobile: '' }))

const [getData, api] = useApi(() => new Promise(r => setTimeout(r, 1000)), {
  loading: ref(false),
  params: () => query,
  formatRequest: (params) => ({ ...params, name: 'new_name' }),
  formatResponse: (res: any) => res?.data,
  onSuccess: (data) => {
    dataOperate.merge(data)
  }
})

getData()                // Call API (uses params as request parameters)
getData({ id: '1' })     // Override merged into params
api.loading              // Loading state
api.status               // 'idle' | 'loading' | 'success' | 'error'
api.cancel()             // Cancel current request

Scenarios

Block Requests

const [getData, api] = useApi(fetchApi, {
  loading: ref(false),
  params: () => query,
  before: async (params) => {
    return false  // Block request (or Promise.reject())
  },
  formatResponse: (res: any) => res?.data,
  onSuccess: (data) => { dataOperate.merge(data) }
})

One API, Multiple Purposes (Multiple loadingKeys)

const [getData, api] = useApi(fetchList, {
  loading: reactive({ data: false, load: false, refresh: false }),
  params: () => query,
  formatResponse: (res: any) => res?.data,
  onSuccess: (data, { loadingKey }) => {
    if (loadingKey === 'data') { /* Initial load */ }
    else if (loadingKey === 'load') { /* Load more */ }
    else if (loadingKey === 'refresh') { /* Refresh */ }
  }
})

getData({}, 'data')      // loading.data = true
getData({}, 'load')      // loading.load = true
getData({}, 'refresh')   // loading.refresh = true

Prevent Duplicate Submissions

const [submit, api] = useApi(fetchSubmit, {
  allowRepeat: false,
  loading: ref(false),
  params: () => form,
  formatResponse: (res: any) => res?.data,
  onSuccess: (data) => { dataOperate.merge(data) }
})

submit()  // Rejected while previous request is in progress

Singleton Mode

const [getConfig, api] = useApi(fetchConfig, {
  singleton: true,
  loading: ref(false),
  formatResponse: (res: any) => res?.data,
  onSuccess: (data) => { configOperate.merge(data) }
})

getConfig()  // First call: makes request
getConfig()  // Subsequent calls: returns cached result

Cancellable Requests

const [getData, api] = useApi(() => {
  let timer: any
  const pm: any = new Promise((resolve, reject) => {
    timer = setTimeout(() => resolve(true), 5000)
  })
  pm.cancel = () => { clearTimeout(timer); reject() }
  return pm
}, {
  cancelPrevious: true,
  loading: ref(false),
  params: () => query,
  formatResponse: (res: any) => res?.data,
  onSuccess: (data) => { dataOperate.merge(data) }
})

getData()
api.cancel()  // Manually cancel current request

Toast Loading Integration

import { Toast } from 'some-ui-library'

const [getData, api] = useApi(fetchData, {
  loading: ref(false),
  params: () => query,
  formatResponse: (res: any) => res?.data,
  onLoadingStart: (params, loadingKey) => {
    return Toast.loading('Loading...')  // Returns Toast instance
  },
  onLoadingEnd: (toast) => {
    toast?.clear()  // Type-safe, toast is Toast instance
  },
  onSuccess: (data) => { dataOperate.merge(data) }
})

API

Parameters

| Parameter | Return | | - | :-: | | useApi(api: () => Promise, options?: object) | [request function, operate object] |

Options

| Parameter | Description | Type | Default | | - | - | :-: | :-: | | cancelPrevious | Cancel previous unfinished request on consecutive calls, keep only latest result | boolean | true | | allowRepeat | Whether to allow duplicate calls | boolean | true | | singleton | Singleton mode, subsequent calls return first successful result | boolean | false | | loading | Loading state object | object | - | | params | Parameter source definition, override params are merged into this object | () => object | - | | before | Pre-request hook, return false or Promise.reject to block | (params) => boolean | void | Promise | - | | formatRequest | Request parameter transformation, output passed to api function | (params) => OQ | - | | formatResponse | Response data transformation, converts raw response to component-ready data | (res) => RD | - | | onSuccess | Success callback | (data: RD, context: { loadingKey, res, params }) => void | - | | onError | Error callback | (err) => void | - | | onLoadingStart | Loading start callback, return value passed to onLoadingEnd | (params, loadingKey) => LS | - | | onLoadingEnd | Loading end callback, receives onLoadingStart return value | (state: LS) => void | - |

Request Function

| Parameter | Type | Description | | - | :-: | - | | override | Partial<Q> | Merged into params (only existing keys) | | loadingKey | LK (key of loading object) | Specifies which loading key to use, default 'value' |

Return value: Promise<{ res: R, data: RD, loadingKey: LK }>

  • res — Raw API response
  • data — Data after formatResponse transformation
  • loadingKey — Loading key for this request

Operate Object

| Property/Method | Type | Description | | - | :-: | - | | loading | L | Loading state object (the one passed in, e.g. ref(false) or reactive({...})) | | status | 'idle' | 'loading' | 'success' | 'error' | Request status: idle = not started, loading = in progress, success = succeeded, error = failed | | result | { res: R, data: RD, loadingKey: LK } | Latest request result | | error | string | Error | undefined | Error info, only set when status is error | | cancel | () => Promise<void> | Cancel current request |


Documentation