Overview
TailStack uses React hooks and custom hook patterns for state management, providing a lightweight alternative to external state management libraries for most use cases.
State Management Philosophy
TailStack follows these principles:
Local state first - Keep state close to where it’s used
Custom hooks for shared logic - Encapsulate reusable state patterns
Context for global state - Use React Context for theme, auth, etc.
URL as state - Leverage React Router for navigation state
Server state separation - Use appropriate tools for API data
Custom Hooks
TailStack includes several custom hooks for common patterns:
useTheme Hook
Manages theme state with localStorage persistence and system preference detection:
packages/core/source/frontend/src/hooks/use-theme.ts
import { useEffect , useState } from 'react' ;
import type { Theme } from '@/types/theme' ;
export function useTheme () {
const [ theme , setTheme ] = useState < Theme >(() => {
// Check localStorage first
const stored = localStorage . getItem ( 'theme' ) as Theme | null ;
if ( stored ) return stored ;
// Check system preference
if ( window . matchMedia ( '(prefers-color-scheme: dark)' ). matches ) {
return 'dark' ;
}
return 'light' ;
});
useEffect (() => {
const root = document . documentElement ;
if ( theme === 'dark' ) {
root . classList . add ( 'dark' );
} else {
root . classList . remove ( 'dark' );
}
localStorage . setItem ( 'theme' , theme );
}, [ theme ]);
const toggleTheme = () => {
setTheme (( prev ) => ( prev === 'light' ? 'dark' : 'light' ));
};
return { theme , toggleTheme , setTheme };
}
The useTheme hook initializes from localStorage, falls back to system preferences, and persists changes automatically.
Usage Example
import { useTheme } from '@/hooks/use-theme' ;
function ThemeToggle () {
const { theme , toggleTheme } = useTheme ();
return (
< button onClick = { toggleTheme } >
Current theme: { theme }
</ button >
);
}
useNavigation Hook
Provides navigation state and active route detection:
packages/core/source/frontend/src/hooks/use-navigation.ts
import { useLocation } from 'react-router-dom' ;
export function useNavigation () {
const location = useLocation ();
const isActive = ( path : string , exact = true ) => {
if ( exact ) {
return location . pathname === path ;
}
if ( path === '/docs' ) {
return location . pathname . startsWith ( '/docs' );
}
return location . pathname . startsWith ( path );
};
return { location , isActive };
}
Usage Example
import { useNavigation } from '@/hooks/use-navigation' ;
import { Link } from 'react-router-dom' ;
function NavLink ({ to , children } : { to : string ; children : React . ReactNode }) {
const { isActive } = useNavigation ();
const active = isActive ( to , to !== '/docs' );
return (
< Link
to = { to }
className = { active ? 'text-foreground font-medium' : 'text-muted-foreground' }
>
{ children }
</ Link >
);
}
useToggle Hook
Simplifies boolean state management with toggle functionality:
packages/core/source/frontend/src/hooks/use-toggle.ts
import { useState , useCallback } from 'react' ;
export function useToggle ( initialValue = false ) {
const [ value , setValue ] = useState ( initialValue );
const toggle = useCallback (() => {
setValue (( v ) => ! v );
}, []);
return [ value , toggle , setValue ] as const ;
}
Usage Example
import { useToggle } from '@/hooks/use-toggle' ;
function MobileMenu () {
const [ isOpen , toggle , setIsOpen ] = useToggle ( false );
return (
<>
< button onClick = { toggle } > Toggle Menu </ button >
{ isOpen && (
< nav >
< a href = "/home" onClick = { () => setIsOpen ( false ) } > Home </ a >
< a href = "/docs" onClick = { () => setIsOpen ( false ) } > Docs </ a >
</ nav >
) }
</>
);
}
The useToggle hook returns a tuple: [value, toggle, setValue] for maximum flexibility.
Local State Management
For component-specific state, use React’s built-in hooks:
useState
import { useState } from 'react' ;
function Counter () {
const [ count , setCount ] = useState ( 0 );
return (
< div >
< p > Count: { count } </ p >
< button onClick = { () => setCount ( count + 1 ) } > Increment </ button >
< button onClick = { () => setCount ( 0 ) } > Reset </ button >
</ div >
);
}
useReducer
For complex state logic:
import { useReducer } from 'react' ;
type State = { count : number ; step : number };
type Action =
| { type : 'increment' }
| { type : 'decrement' }
| { type : 'setStep' ; payload : number };
function reducer ( state : State , action : Action ) : State {
switch ( action . type ) {
case 'increment' :
return { ... state , count: state . count + state . step };
case 'decrement' :
return { ... state , count: state . count - state . step };
case 'setStep' :
return { ... state , step: action . payload };
default :
return state ;
}
}
function AdvancedCounter () {
const [ state , dispatch ] = useReducer ( reducer , { count: 0 , step: 1 });
return (
< div >
< p > Count: { state . count } </ p >
< button onClick = { () => dispatch ({ type: 'increment' }) } > + { state . step } </ button >
< button onClick = { () => dispatch ({ type: 'decrement' }) } > - { state . step } </ button >
< input
type = "number"
value = { state . step }
onChange = { ( e ) => dispatch ({ type: 'setStep' , payload: Number ( e . target . value ) }) }
/>
</ div >
);
}
Handle form state with controlled components:
import { useState , FormEvent } from 'react' ;
import { Input } from '@/components/ui/input' ;
import { Button } from '@/components/ui/button' ;
interface FormData {
email : string ;
password : string ;
}
function LoginForm () {
const [ formData , setFormData ] = useState < FormData >({
email: '' ,
password: '' ,
});
const [ errors , setErrors ] = useState < Partial < FormData >>({});
const handleChange = ( field : keyof FormData ) => ( e : React . ChangeEvent < HTMLInputElement >) => {
setFormData ( prev => ({ ... prev , [field]: e . target . value }));
// Clear error when user types
if ( errors [ field ]) {
setErrors ( prev => ({ ... prev , [field]: undefined }));
}
};
const validate = () : boolean => {
const newErrors : Partial < FormData > = {};
if ( ! formData . email . includes ( '@' )) {
newErrors . email = 'Invalid email' ;
}
if ( formData . password . length < 8 ) {
newErrors . password = 'Password must be 8+ characters' ;
}
setErrors ( newErrors );
return Object . keys ( newErrors ). length === 0 ;
};
const handleSubmit = ( e : FormEvent ) => {
e . preventDefault ();
if ( validate ()) {
// Submit form
console . log ( 'Form submitted:' , formData );
}
};
return (
< form onSubmit = { handleSubmit } className = "space-y-4" >
< div >
< Input
type = "email"
value = { formData . email }
onChange = { handleChange ( 'email' ) }
placeholder = "Email"
/>
{ errors . email && < p className = "text-sm text-destructive" > { errors . email } </ p > }
</ div >
< div >
< Input
type = "password"
value = { formData . password }
onChange = { handleChange ( 'password' ) }
placeholder = "Password"
/>
{ errors . password && < p className = "text-sm text-destructive" > { errors . password } </ p > }
</ div >
< Button type = "submit" > Login </ Button >
</ form >
);
}
Global State with Context
For application-wide state, use React Context:
import { createContext , useContext , useState , ReactNode } from 'react' ;
interface User {
id : string ;
name : string ;
email : string ;
}
interface AuthContextType {
user : User | null ;
login : ( user : User ) => void ;
logout : () => void ;
isAuthenticated : boolean ;
}
const AuthContext = createContext < AuthContextType | undefined >( undefined );
export function AuthProvider ({ children } : { children : ReactNode }) {
const [ user , setUser ] = useState < User | null >( null );
const login = ( user : User ) => {
setUser ( user );
localStorage . setItem ( 'user' , JSON . stringify ( user ));
};
const logout = () => {
setUser ( null );
localStorage . removeItem ( 'user' );
};
const value = {
user ,
login ,
logout ,
isAuthenticated: !! user ,
};
return < AuthContext.Provider value = { value } > { children } </ AuthContext.Provider > ;
}
export function useAuth () {
const context = useContext ( AuthContext );
if ( context === undefined ) {
throw new Error ( 'useAuth must be used within an AuthProvider' );
}
return context ;
}
Using the Context
import { useAuth } from '@/contexts/auth-context' ;
function App () {
return (
< AuthProvider >
< AppContent />
</ AuthProvider >
);
}
function Profile () {
const { user , logout , isAuthenticated } = useAuth ();
if ( ! isAuthenticated ) {
return < LoginPage /> ;
}
return (
< div >
< h1 > Welcome, { user . name } </ h1 >
< button onClick = { logout } > Logout </ button >
</ div >
);
}
Async State Management
Handle API calls and async operations:
import { useState , useEffect } from 'react' ;
interface AsyncState < T > {
data : T | null ;
loading : boolean ;
error : Error | null ;
}
function useAsync < T >( asyncFn : () => Promise < T >, deps : any [] = []) {
const [ state , setState ] = useState < AsyncState < T >>({
data: null ,
loading: true ,
error: null ,
});
useEffect (() => {
let cancelled = false ;
setState ({ data: null , loading: true , error: null });
asyncFn ()
. then ( data => {
if ( ! cancelled ) {
setState ({ data , loading: false , error: null });
}
})
. catch ( error => {
if ( ! cancelled ) {
setState ({ data: null , loading: false , error });
}
});
return () => {
cancelled = true ;
};
}, deps );
return state ;
}
// Usage
function UserProfile ({ userId } : { userId : string }) {
const { data , loading , error } = useAsync (
() => fetch ( `/api/users/ ${ userId } ` ). then ( r => r . json ()),
[ userId ]
);
if ( loading ) return < div > Loading... </ div > ;
if ( error ) return < div > Error: { error . message } </ div > ;
if ( ! data ) return null ;
return < div > User: { data . name } </ div > ;
}
State Management Best Practices
Start with local state - Lift state only when necessary
Use custom hooks - Encapsulate reusable state logic
Minimize re-renders - Use useCallback and useMemo appropriately
Separate concerns - UI state vs. server state vs. URL state
Type your state - Use TypeScript for type-safe state management
Handle loading and error states - Provide good UX for async operations
Persist important state - Use localStorage for user preferences
Creating Custom Hooks
Follow this pattern for reusable hooks:
import { useState , useEffect } from 'react' ;
// Hook for managing local storage state
function useLocalStorage < T >( key : string , initialValue : T ) {
const [ value , setValue ] = useState < T >(() => {
try {
const item = localStorage . getItem ( key );
return item ? JSON . parse ( item ) : initialValue ;
} catch {
return initialValue ;
}
});
useEffect (() => {
try {
localStorage . setItem ( key , JSON . stringify ( value ));
} catch ( error ) {
console . error ( 'Failed to save to localStorage:' , error );
}
}, [ key , value ]);
return [ value , setValue ] as const ;
}
// Usage
function Settings () {
const [ notifications , setNotifications ] = useLocalStorage ( 'notifications' , true );
return (
< label >
< input
type = "checkbox"
checked = { notifications }
onChange = { ( e ) => setNotifications ( e . target . checked ) }
/>
Enable notifications
</ label >
);
}
Next Steps
Components Learn about component structure and shadcn UI components
Routing Explore React Router setup and navigation patterns