SPB Git

spb/findata

Public
3.1 KB · 113 lines typescript
1"use client";2import { useCallback, useEffect, useRef, useState } from "react";34/** Generic async loader with abort + refetch. `deps` re-run the fetch. */5export function useAsync<T>(fn: (signal: AbortSignal) => Promise<T>, deps: unknown[], opts: { enabled?: boolean } = {}) {6  const enabled = opts.enabled ?? true;7  const [data, setData] = useState<T | null>(null);8  const [error, setError] = useState<Error | null>(null);9  const [loading, setLoading] = useState<boolean>(enabled);10  const [tick, setTick] = useState(0);11  const fnRef = useRef(fn);12  fnRef.current = fn;1314  useEffect(() => {15    if (!enabled) {16      setLoading(false);17      return;18    }19    const ctl = new AbortController();20    let alive = true;21    setLoading(true);22    fnRef23      .current(ctl.signal)24      .then((d) => {25        if (!alive) return;26        setData(d);27        setError(null);28      })29      .catch((e) => {30        if (!alive || ctl.signal.aborted) return;31        setError(e instanceof Error ? e : new Error(String(e)));32      })33      .finally(() => {34        if (alive) setLoading(false);35      });36    return () => {37      alive = false;38      ctl.abort();39    };40    // eslint-disable-next-line react-hooks/exhaustive-deps41  }, [...deps, tick, enabled]);4243  const reload = useCallback(() => setTick((t) => t + 1), []);44  return { data, error, loading, reload, setData };45}4647export function useDebounced<T>(value: T, ms = 300): T {48  const [v, setV] = useState(value);49  useEffect(() => {50    const t = window.setTimeout(() => setV(value), ms);51    return () => window.clearTimeout(t);52  }, [value, ms]);53  return v;54}5556/** Read a query-string parameter client-side (static export cannot use dynamic routes). */57export function useQueryParam(name: string): string | null {58  const [v, setV] = useState<string | null>(null);59  useEffect(() => {60    const read = () => setV(new URLSearchParams(window.location.search).get(name));61    read();62    window.addEventListener("popstate", read);63    return () => window.removeEventListener("popstate", read);64  }, [name]);65  return v;66}6768export function useInterval(cb: () => void, ms: number | null) {69  const ref = useRef(cb);70  ref.current = cb;71  useEffect(() => {72    if (ms === null) return;73    const id = window.setInterval(() => ref.current(), ms);74    return () => window.clearInterval(id);75  }, [ms]);76}7778export function useMediaQuery(q: string): boolean {79  const [m, setM] = useState(false);80  useEffect(() => {81    const mq = window.matchMedia(q);82    const on = () => setM(mq.matches);83    on();84    mq.addEventListener("change", on);85    return () => mq.removeEventListener("change", on);86  }, [q]);87  return m;88}8990export function useLocalStorage<T>(key: string, initial: T): [T, (v: T) => void] {91  const [v, setV] = useState<T>(initial);92  useEffect(() => {93    try {94      const raw = window.localStorage.getItem(key);95      if (raw !== null) setV(JSON.parse(raw) as T);96    } catch {97      /* ignore */98    }99  }, [key]);100  const set = useCallback(101    (nv: T) => {102      setV(nv);103      try {104        window.localStorage.setItem(key, JSON.stringify(nv));105      } catch {106        /* ignore */107      }108    },109    [key],110  );111  return [v, set];112}113
Files · 6437fd798ded13a52b8fed8b7d8e78b700fbceec
Menu

Theme