Framework-agnostic URL-synchronized state management with built-in state sharing between components. Uses signals to share state efficiently between components.
- π State sharing between components using signals
- π URL synchronization with component state
- π§ Browser navigation (back/forward) support
- π€ Framework agnostic design
- π¦ TypeScript included
- πͺΆ Small bundle size (~1KB)
- πͺ No dependencies
npm install @bhammond/react-stateful
- React 16.8+
import { useQueryState } from '@bhammond/react-stateful';
function SearchComponent() {
const params = new URLSearchParams(window.location.search);
const [query, setQuery] = useQueryState('q', params);
return (
<input
value={query ?? ''}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
Components using the same key will share state through signals:
import { useQueryState } from '@bhammond/react-stateful';
function SearchInput({ params }) {
const [query, setQuery] = useQueryState('q', params);
return <input value={query ?? ''} onChange={e => setQuery(e.target.value)} />;
}
function SearchResults({ params }) {
const [query] = useQueryState('q', params);
return <div>Results for: {query}</div>;
}
function FilterStatus({ params }) {
const [query] = useQueryState('q', params);
return <div>Current filter: {query || 'None'}</div>;
}
function SearchPage() {
const params = new URLSearchParams(window.location.search);
return (
<div>
<SearchInput params={params} />
<FilterStatus params={params} />
<SearchResults params={params} />
</div>
);
}
The hook works with complex objects and maintains type safety:
interface Filters {
search: string;
category: string;
sortBy: string;
page: number;
}
const DEFAULT_FILTERS: Filters = {
search: '',
category: 'all',
sortBy: 'date',
page: 1
};
function FilterPanel({ params }) {
const [filters, setFilters] = useQueryState<Filters>('filters', params, DEFAULT_FILTERS);
const updateFilter = (key: keyof Filters, value: Filters[keyof Filters]) => {
setFilters(current => ({
...current,
[key]: value,
page: key === 'page' ? value : 1
}));
};
return (
<div>
<input
value={filters.search}
onChange={e => updateFilter('search', e.target.value)}
/>
<select
value={filters.category}
onChange={e => updateFilter('category', e.target.value)}
>
{/* options */}
</select>
</div>
);
}
function useQueryState<T = string>(
name: string,
params: ParamsInput,
defaultValue?: T
): [T, (newValue: T | ((prev: T) => T)) => void]
name: string
- URL parameter keyparams: ParamsInput
- Either:URLParamsLike
: An object with aget(key: string): string | null
methodRecordParams
: An object with string or string array values
defaultValue?: T
- Optional default value when parameter is not present
[value, setValue]
- A tuple containing the current value and setter function
import { useSearchParams } from 'react-router-dom';
import { useQueryState } from '@bhammond/react-stateful';
function SearchComponent() {
const [searchParams] = useSearchParams();
const [query, setQuery] = useQueryState('q', searchParams);
return (
<input
value={query ?? ''}
onChange={(e) => setQuery(e.target.value)}
/>
);
}
class CustomParams implements URLParamsLike {
private params: Map<string, string>;
constructor() {
this.params = new Map();
}
get(key: string): string | null {
return this.params.get(key) ?? null;
}
set(key: string, value: string): void {
this.params.set(key, value);
}
}
function Component() {
const params = new CustomParams();
const [value, setValue] = useQueryState('key', params);
// ...
}
Includes TypeScript definitions with full type inference support.
- Signal-based state sharing
- Selective component updates
- Batched URL updates
- Small bundle size
- No external dependencies
Contributions are welcome. Please feel free to submit a Pull Request.
MIT