Skip to content
Getting started

Install @danixsoft/hooks

npm install @danixsoft/hooks

@danixsoft/hooks is a collection of 44 React hooks with no runtime dependencies, written in TypeScript and safe to render on a server. This page takes you from an empty project to a working hook in about two minutes.

Requirements

  • React 18 or later, including React 19. React is a peer dependency, so your app controls the version.
  • Node 18 or later for the build tooling. The hooks themselves run in the browser and have no Node requirement.
  • TypeScript 5 or later if you use TypeScript. Types ship with the package — there is no separate @types install.

Installation

npm install @danixsoft/hooks
# or: pnpm add @danixsoft/hooks
# or: yarn add @danixsoft/hooks
# or: bun add @danixsoft/hooks

That is the whole install. The package has no runtime dependencies, so nothing else enters your lockfile.

Quick start

Every hook is a named export from the package root. Import what you need and call it like any other hook.

components/theme-toggle.tsx
1import { useLocalStorage } from '@danixsoft/hooks';
2
3export function ThemeToggle() {
4  const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'dark');
5
6  return (
7    <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
8      {theme === 'dark' ? '☀️ Light' : '🌙 Dark'}
9    </button>
10  );
11}

Next.js App Router

Hooks only run in Client Components. Add "use client" at the top of any file that calls one — the same rule React applies to useState. Keep that directive as far down the tree as possible so less of your app has to hydrate.

A slightly larger example, combining three hooks to build a debounced, responsive search box with persisted history:

components/search.tsx
1'use client';
2
3import { useState } from 'react';
4import { useDebounce, useLocalStorage, useMediaQuery } from '@danixsoft/hooks';
5
6export function Search() {
7  const [query, setQuery] = useState('');
8  const [history, setHistory] = useLocalStorage<string[]>('search-history', []);
9
10  const debouncedQuery = useDebounce(query, 300);       // one request per pause
11  const isMobile = useMediaQuery('(max-width: 768px)'); // SSR-safe breakpoint
12
13  return (
14    <input
15      value={query}
16      onChange={(event) => setQuery(event.target.value)}
17      placeholder={isMobile ? 'Search' : 'Search everything…'}
18    />
19  );
20}

TypeScript

The package is written in TypeScript and ships its own declarations. In most cases you never write a type annotation — the generics infer from the arguments you pass.

1// Inferred as string, because the initial value is a string.
2const [name, setName] = useLocalStorage('name', 'Ada');
3
4// Inferred as Preferences, because the initial value is a Preferences.
5const [prefs, setPrefs] = useLocalStorage('prefs', { compact: false });
6
7// Annotate explicitly when the type is wider than the initial value.
8const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'dark');
The third case is the one worth remembering: without the annotation, TypeScript infers the literal type "dark" and rejects setTheme("light"). Widening the generic is the fix.

Server-side rendering

Every hook that reads a browser API guards it and returns a stable value during server rendering. That means no window is not defined crash during the build, and no hydration mismatch on the client.

The consequence worth knowing: browser-derived values are only correct after hydration. useWindowSize returns undefined on the server, useMediaQuery returns false, and useLocalStorage returns the initial value until the effect runs. This is deliberate — it is what keeps the first render identical on both sides.

Reserve the space, then fill it
1import { useIsClient } from '@danixsoft/hooks';
2
3function ViewportBadge() {
4  const isClient = useIsClient();
5
6  // Same dimensions before and after, so nothing shifts (and CLS stays at 0).
7  if (!isClient) return <span className="inline-block h-6 w-24" />;
8
9  return <span>{window.innerWidth}px</span>;
10}

When a flash is unacceptable

For something as visible as a theme, showing the default first is not good enough. You need a blocking inline script in the document head — the full technique is in SSR-safe hooks in Next.js.

Tree shaking

The package is published as ES modules and marked sideEffects: false, so any modern bundler — webpack, Vite, Rollup, esbuild, Turbopack — drops the hooks you do not import.

// Only useDebounce reaches your bundle. The other 43 hooks are dropped.
import { useDebounce } from '@danixsoft/hooks';

There is no deep-import path to remember and no /dist/useDebounce convention — importing from the package root is already optimal.

Testing

The hooks are ordinary functions, so renderHook from React Testing Library tests them directly. Nothing has to be mocked to use the library itself.

counter.test.ts
1import { renderHook, act } from '@testing-library/react';
2import { useCounter } from '@danixsoft/hooks';
3
4it('respects the configured maximum', () => {
5  const { result } = renderHook(() => useCounter(0, { max: 2 }));
6
7  act(() => {
8    result.current.increment();
9    result.current.increment();
10    result.current.increment();  // refused
11  });
12
13  expect(result.current.count).toBe(2);
14});
Hooks that read browser APIs need a DOM environment. Set environment: "jsdom" in Vitest, or testEnvironment: "jsdom" in Jest.

Where to go next