Skip to main content

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:
app/layout.tsx
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>
  )
}
1

LoadingProvider

Outermost provider - manages global loading state
2

AudioProvider

Middle layer - handles background audio playback
3

ThemeProvider

Innermost layer - forces light mode theme

LoadingProvider

Manages the initial loading screen state across the application. Location: hooks/use-loading.tsx
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.
hooks/use-device-type.ts
"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>
}

Form State Management

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

1

Use Context for Global State

Only lift state to Context when needed across multiple components.Keep local state in components when possible.
2

Type Safety First

Always define TypeScript interfaces for Context values:
interface MyContextType {
  value: string
  setValue: (value: string) => void
}
3

Error Handling

Throw errors when hooks are used outside providers:
if (!context) throw new Error('Hook must be used within Provider')
4

Cleanup Effects

Always cleanup subscriptions and listeners:
useEffect(() => {
  const handler = () => {}
  window.addEventListener('event', handler)
  return () => window.removeEventListener('event', handler)
}, [])
5

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

Build docs developers (and LLMs) love