@magic_npm/simple-tools
v1.4.0
Published
`simple-tools` is a simple set of tools
Maintainers
Readme
simple-tools
A utility toolkit with
useDataanduseApifor improving code quality and development efficiency
Installation
npm
npm i @magic_npm/simple-toolsyarn
yarn add @magic_npm/simple-toolsWhat 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 bugWith 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: falseblocks consecutive clicks during form submission - Cancel previous requests — Search suggestion scenarios,
cancelPrevious: truekeeps only the latest result - Load once — Config data that doesn't change,
singleton: truecaches the first result - One API, multiple purposes — Same request for load, load more, refresh, differentiated by loadingKey
- Pre-request validation —
beforehook to block requests when validation fails - Data transformation isolation —
formatResponsehandles 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 mergeVue Reactive Data
import { reactive } from 'vue'
const [query, queryOperate] = useData(() => reactive({
id: '',
name: ''
}))
queryOperate.merge({ id: '1' })
query.id // '1' — page updates automaticallyMultiple 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:
- External isolation — After merging
params()withoverride, the result is cloned asparamsSnapshot. This decouples from the external object so thatapi()and its consumers never mutate the originalparamssource. - formatRequest isolation — Before passing to
formatRequest,paramsSnapshotis cloned again. This ensuresformatRequest's output does not alterparamsSnapshot, which is also used bybefore,onLoadingStart, andonSuccess(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) => RDfunction 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 typefunction 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 —
paramscan return a superset of whatapirequires (structural subtyping) - Cannot omit fields —
paramsmust satisfy all required fields ofapi's parameter type - Cannot mismatch types — field types must be compatible
- formatResponse is required for typed onSuccess — without
formatResponse,onSuccess(data)will beunknown; provideformatResponseto inferRD
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 requestScenarios
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 = truePrevent 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 progressSingleton 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 resultCancellable 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 requestToast 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 responsedata— Data after formatResponse transformationloadingKey— 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 |
