State Management Strategy
The NJ Rajat Mahotsav platform uses React Context API for global state management, combined with custom hooks for reusable logic. This approach provides a lightweight, type-safe solution without additional dependencies.
No external state management libraries (Redux, Zustand, etc.) are used. Context API handles all global state needs efficiently.
Global Providers
Three main context providers are nested in the root layout:
export default function RootLayout ({ children } : { children : React . ReactNode }) {
return (
< html lang = "en" suppressHydrationWarning >
< body >
< LoadingProvider >
< AudioProvider >
< ThemeProvider attribute = "class" defaultTheme = "light" forcedTheme = "light" >
{ /* App content */ }
</ ThemeProvider >
</ AudioProvider >
</ LoadingProvider >
</ body >
</ html >
)
}
LoadingProvider
Outermost provider - manages global loading state
AudioProvider
Middle layer - handles background audio playback
ThemeProvider
Innermost layer - forces light mode theme
LoadingProvider
Manages the initial loading screen state across the application.
Location: hooks/use-loading.tsx
"use client"
import { createContext , useContext , useState , ReactNode } from "react"
interface LoadingContextType {
isLoading : boolean
setIsLoading : ( value : boolean ) => void
}
const LoadingContext = createContext < LoadingContextType | null >( null )
export function LoadingProvider ({ children } : { children : ReactNode }) {
const [ isLoading , setIsLoading ] = useState ( false )
return (
< LoadingContext.Provider value = { { isLoading , setIsLoading } } >
{ children }
</ LoadingContext.Provider >
)
}
export function useLoading () {
const context = useContext ( LoadingContext )
if ( ! context ) throw new Error ( 'useLoading must be used within LoadingProvider' )
return context
}
Usage Example
components/atoms/loading-screen.tsx
import { useLoading } from "@/hooks/use-loading"
export default function LoadingScreen () {
const { isLoading , setIsLoading } = useLoading ()
const handleEnter = () => {
setIsLoading ( false ) // Hide loading screen
}
return (
< AnimatePresence >
{ isLoading && (
< motion.div exit = { { opacity: 0 } } >
{ /* Loading content */ }
</ motion.div >
) }
</ AnimatePresence >
)
}
AudioProvider
Provides background audio control with fade effects and user consent management.
Location: contexts/audio-context.tsx
contexts/audio-context.tsx
"use client"
import { createContext , useContext , useEffect , useRef , useState , ReactNode } from 'react'
import { usePathname } from 'next/navigation'
interface AudioContextType {
play : () => void
pause : () => void
fadeOut : ( duration ?: number ) => void
fadeToVolume : ( targetVolume : number , duration ?: number ) => void
toggle : () => void
isPlaying : boolean
isLoaded : boolean
hasUserConsent : boolean
grantConsent : () => void
}
const AudioContext = createContext < AudioContextType | null >( null )
export function AudioProvider ({ children } : { children : ReactNode }) {
const audioRef = useRef < HTMLAudioElement | null >( null )
const [ isPlaying , setIsPlaying ] = useState ( false )
const [ isLoaded , setIsLoaded ] = useState ( false )
const hasUserConsentRef = useRef ( false )
const [ hasUserConsent , setHasUserConsent ] = useState ( false )
const fadeIntervalRef = useRef < NodeJS . Timeout | null >( null )
const targetVolumeRef = useRef ( 1 )
const pathname = usePathname ()
useEffect (() => {
audioRef . current = new Audio ( 'https://cdn.njrajatmahotsav.com/audio_files/prathna%2Banthem.mp3' )
audioRef . current . preload = 'auto'
audioRef . current . volume = 0 // Start muted
const audio = audioRef . current
const handleCanPlayThrough = () => setIsLoaded ( true )
const handleEnded = () => setIsPlaying ( false )
audio . addEventListener ( 'canplaythrough' , handleCanPlayThrough )
audio . addEventListener ( 'ended' , handleEnded )
return () => {
if ( fadeIntervalRef . current ) clearInterval ( fadeIntervalRef . current )
audio . pause ()
audio . removeEventListener ( 'canplaythrough' , handleCanPlayThrough )
audio . removeEventListener ( 'ended' , handleEnded )
}
}, [])
const grantConsent = () => {
hasUserConsentRef . current = true
setHasUserConsent ( true )
}
const play = () => {
if ( audioRef . current && hasUserConsentRef . current ) {
audioRef . current . volume = targetVolumeRef . current
audioRef . current . play ()
setIsPlaying ( true )
}
}
const pause = () => {
audioRef . current ?. pause ()
setIsPlaying ( false )
}
const fadeOut = ( duration = 1000 ) => {
if ( ! audioRef . current || ! isPlaying || ! hasUserConsentRef . current ) return
if ( fadeIntervalRef . current ) clearInterval ( fadeIntervalRef . current )
const audio = audioRef . current
const startVolume = audio . volume
const steps = 50
const stepTime = duration / steps
const volumeStep = startVolume / steps
fadeIntervalRef . current = setInterval (() => {
if ( audio . volume > volumeStep ) {
audio . volume = Math . max ( 0 , audio . volume - volumeStep )
} else {
audio . volume = 0
audio . pause ()
setIsPlaying ( false )
if ( fadeIntervalRef . current ) clearInterval ( fadeIntervalRef . current )
}
}, stepTime )
}
const fadeToVolume = ( targetVolume : number , duration = 1000 ) => {
if ( ! audioRef . current || ! hasUserConsentRef . current ) return
if ( fadeIntervalRef . current ) clearInterval ( fadeIntervalRef . current )
const audio = audioRef . current
const startVolume = audio . volume
const volumeDiff = targetVolume - startVolume
const steps = 50
const stepTime = duration / steps
const volumeStep = volumeDiff / steps
targetVolumeRef . current = targetVolume
fadeIntervalRef . current = setInterval (() => {
const newVolume = audio . volume + volumeStep
if (( volumeStep > 0 && newVolume < targetVolume ) || ( volumeStep < 0 && newVolume > targetVolume )) {
audio . volume = newVolume
} else {
audio . volume = targetVolume
if ( fadeIntervalRef . current ) clearInterval ( fadeIntervalRef . current )
}
}, stepTime )
}
const toggle = () => {
if ( isPlaying ) {
pause ()
} else {
grantConsent ()
if ( audioRef . current ) {
audioRef . current . volume = targetVolumeRef . current
audioRef . current . play ()
setIsPlaying ( true )
}
}
}
return (
< AudioContext.Provider value = { {
play ,
pause ,
fadeOut ,
fadeToVolume ,
toggle ,
isPlaying ,
isLoaded ,
hasUserConsent ,
grantConsent
} } >
{ children }
</ AudioContext.Provider >
)
}
export function useAudioContext () {
const context = useContext ( AudioContext )
if ( ! context ) throw new Error ( 'useAudioContext must be used within AudioProvider' )
return context
}
Key Features
User Consent Respects browser autoplay policies by requiring user interaction before playing audio
Fade Effects Smooth volume transitions with fadeOut() and fadeToVolume() for better UX
Persistent Audio Audio continues across page navigation (commented code shows path-based control)
Loading State Tracks audio loading state with isLoaded flag
Usage Example
components/audio-player.tsx
import { useAudioContext } from "@/contexts/audio-context"
import { Volume2 , VolumeX } from "lucide-react"
export function AudioPlayer () {
const { toggle , isPlaying , isLoaded } = useAudioContext ()
return (
< button onClick = { toggle } disabled = { ! isLoaded } >
{ isPlaying ? < Volume2 /> : < VolumeX /> }
</ button >
)
}
ThemeProvider
Wraps next-themes for theme management, forced to light mode for this project.
Location: components/atoms/theme-provider.tsx
components/atoms/theme-provider.tsx
"use client"
import * as React from 'react'
import { ThemeProvider as NextThemesProvider , type ThemeProviderProps } from 'next-themes'
export function ThemeProvider ({ children , ... props } : ThemeProviderProps ) {
return < NextThemesProvider { ... props } > { children } </ NextThemesProvider >
}
Root layout configuration:
< ThemeProvider attribute = "class" defaultTheme = "light" forcedTheme = "light" >
{ children }
</ ThemeProvider >
The theme is forced to light mode (forcedTheme="light") as the design system is optimized for light mode only.
Custom Hooks
Reusable logic extracted into custom hooks for better code organization.
Location: hooks/
useDeviceType
Detects device type based on viewport width.
"use client"
import { useState , useEffect } from 'react'
export type DeviceType = 'mobile' | 'tablet' | 'desktop'
export function useDeviceType () : DeviceType {
const [ deviceType , setDeviceType ] = useState < DeviceType | null >( null )
useEffect (() => {
const checkDevice = () => {
const width = window . innerWidth
if ( width <= 768 ) {
setDeviceType ( 'mobile' )
} else if ( width <= 1024 ) {
setDeviceType ( 'tablet' )
} else {
setDeviceType ( 'desktop' )
}
}
checkDevice ()
window . addEventListener ( 'resize' , checkDevice )
return () => window . removeEventListener ( 'resize' , checkDevice )
}, [])
return deviceType ?? 'desktop'
}
Usage:
import { useDeviceType } from "@/hooks/use-device-type"
export function ResponsiveComponent () {
const deviceType = useDeviceType ()
if ( deviceType === 'mobile' ) {
return < MobileView />
}
return < DesktopView />
}
useIntersectionObserver
Triggers scroll-based animations when elements enter viewport.
Location: hooks/use-intersection-observer.ts
Usage:
import { useIntersectionObserver } from "@/hooks/use-intersection-observer"
export function AnimatedSection () {
const [ ref , isVisible ] = useIntersectionObserver ({
threshold: 0.1 ,
triggerOnce: true
})
return (
< div ref = { ref } className = { isVisible ? 'animate-fade-in' : 'opacity-0' } >
{ /* Content */ }
</ div >
)
}
useToast
Manages toast notifications (shadcn/ui integration).
Location: hooks/use-toast.ts
import { useToast } from "@/hooks/use-toast"
export function FormComponent () {
const { toast } = useToast ()
const handleSubmit = async () => {
try {
// Submit form
toast ({
title: "Success" ,
description: "Form submitted successfully"
})
} catch ( error ) {
toast ({
title: "Error" ,
description: "Failed to submit form" ,
variant: "destructive"
})
}
}
return < form onSubmit = { handleSubmit } > ... </ form >
}
Forms use react-hook-form with zod validation:
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
const formSchema = z . object ({
name: z . string (). min ( 2 , "Name must be at least 2 characters" ),
email: z . string (). email ( "Invalid email address" ),
phone: z . string (). regex ( / ^ \+ ? [ 1-9 ] \d {1,14} $ / , "Invalid phone number" )
})
type FormData = z . infer < typeof formSchema >
export function RegistrationForm () {
const form = useForm < FormData >({
resolver: zodResolver ( formSchema ),
defaultValues: {
name: "" ,
email: "" ,
phone: ""
}
})
const onSubmit = async ( data : FormData ) => {
// Submit to Supabase
}
return (
< form onSubmit = { form . handleSubmit ( onSubmit ) } >
{ /* Form fields */ }
</ form >
)
}
Best Practices
Use Context for Global State
Only lift state to Context when needed across multiple components. Keep local state in components when possible.
Type Safety First
Always define TypeScript interfaces for Context values: interface MyContextType {
value : string
setValue : ( value : string ) => void
}
Error Handling
Throw errors when hooks are used outside providers: if ( ! context ) throw new Error ( 'Hook must be used within Provider' )
Cleanup Effects
Always cleanup subscriptions and listeners: useEffect (() => {
const handler = () => {}
window . addEventListener ( 'event' , handler )
return () => window . removeEventListener ( 'event' , handler )
}, [])
Memoize Expensive Calculations
Use useMemo and useCallback to prevent unnecessary re-renders: const memoizedValue = useMemo (() => expensiveCalculation ( dep ), [ dep ])
const memoizedCallback = useCallback (() => {}, [ dep ])
Next Steps
Styling System Learn about Tailwind configuration and CSS variables
Component Structure Explore the atomic design pattern