Skip to main content
Beat App follows a modular component architecture with clear separation between page-level containers, reusable UI components, and layout wrappers. This guide explores the component hierarchy and design patterns used throughout the application.

Component Categories

Components are organized into three primary categories:

Pages

Route-level components that handle data fetching and composition

Layouts

Reusable page structure wrappers

Components

Presentational UI components

Directory Structure

src/
├── components/              # Reusable UI components
│   ├── Player.jsx          # Audio player controls
│   ├── QueueDrawer.jsx     # Playback queue sidebar
│   ├── TrackList.jsx       # Track listing with play controls
│   ├── Sidebar.jsx         # Main navigation sidebar
│   ├── SearchInput.jsx     # Search bar component
│   ├── SearchTabs.jsx      # Search result tabs
│   ├── SearchAllResults.jsx
│   ├── SearchTracksResults.jsx
│   ├── SearchAlbumsResults.jsx
│   ├── SearchArtistsResults.jsx
│   ├── AlbumGrid.jsx       # Grid layout for albums
│   ├── Charts.jsx          # Charts display component
│   ├── HorizontalScroll.jsx # Horizontal scrolling container
│   └── EqualizerIcon/      # Animated equalizer icon
│       └── EqualizerIcon.jsx
├── layouts/                 # Page layout wrappers
│   ├── PageLayout.jsx      # Standard page wrapper
│   ├── PageHeader.jsx      # Page header with title/actions
│   └── PageContent.jsx     # Page content container
└── pages/                   # Route-level page components
    ├── HomePage.jsx        # Landing page
    ├── ExplorePage.jsx     # Explore/discover page
    ├── ChartsPage.jsx      # Music charts
    ├── LibraryPage.jsx     # User library
    ├── SearchPage.jsx      # Search landing
    ├── SearchAllPage.jsx   # All search results
    ├── SearchTracksPage.jsx
    ├── SearchAlbumsPage.jsx
    ├── SearchArtistsPage.jsx
    ├── AlbumPage.jsx       # Album detail view
    ├── ArtistPage.jsx      # Artist detail view
    └── ArtistAlbumsPage.jsx # Artist's albums list

Core Components

App Component

The root App.jsx establishes the application shell with persistent UI elements:
import { Outlet } from "react-router-dom";
import Player from "./components/Player";
import QueueDrawer from "./components/QueueDrawer";
import Sidebar from "./components/Sidebar";

function App() {
  return (
    <Box sx={{ display: "flex", height: "100vh" }}>
      {/* Sidebar - persistent across routes */}
      <Sidebar mobileOpen={mobileOpen} onMobileToggle={handleMobileToggle} />

      <Box sx={{ display: "flex", flexDirection: "column", flexGrow: 1 }}>
        {/* Top bar with search */}
        <Box component="header">
          <SearchInput />
          <IconButton onClick={toggleTheme}>
            {isDarkMode ? <LightModeIcon /> : <DarkModeIcon />}
          </IconButton>
        </Box>

        {/* Main content area */}
        <Box sx={{ display: "flex", flexGrow: 1 }}>
          <Box component="main">
            <Outlet /> {/* Route content renders here */}
          </Box>
          <QueueDrawer /> {/* Queue drawer */}
        </Box>

        {/* Player bar - persistent across routes */}
        <Box className="player-container">
          <Player />
        </Box>
      </Box>
    </Box>
  );
}
Location: src/App.jsx

Layout Components

PageLayout

Provides consistent page structure:
import PageLayout from '../layouts/PageLayout';

function MyPage() {
  return (
    <PageLayout>
      <PageHeader title="My Page" />
      <PageContent>
        {/* Page content here */}
      </PageContent>
    </PageLayout>
  );
}
Main page wrapper that provides:
  • Consistent padding and spacing
  • Responsive layout adjustments
  • Background styling
Location: src/layouts/PageLayout.jsx
Content area wrapper providing:
  • Scrollable content region
  • Proper spacing
  • Responsive behavior
Location: src/layouts/PageContent.jsx

Player Component

The Player component is the heart of the audio experience, connecting store state with UI controls:
import { useStore } from "@nanostores/react";
import { playerStore, playerActions } from "../stores/playerStore";

export default function Player() {
  const { isPlaying, duration, currentTime, currentTrack, isLoading } =
    useStore(playerStore);

  const [muted, setMuted] = useState(false);
  const [volume, setVolume] = useState(100);
  const [liked, setLiked] = useState(false);

  // Sync volume with audio element
  useEffect(() => {
    audioEl.volume = volume / 100;
  }, [volume]);

  // Check like status when track changes
  useEffect(() => {
    if (currentTrack?.trackId) {
      isLiked(currentTrack.trackId).then(setLiked);
      addRecentPlay(currentTrack);
    }
  }, [currentTrack?.trackId]);

  return (
    <div className="player-bar">
      {/* Track info with album art */}
      <div className="track-info">
        <Box sx={{ width: 56, height: 56 }}>
          <img src={`${PROXY_URL}${currentTrack.thumbnailUrl}`} />
        </Box>
        <Box>
          <p>{currentTrack?.title}</p>
          <p>{currentTrack?.artists[0]?.name}</p>
        </Box>
        <IconButton onClick={handleToggleLike}>
          {liked ? <FavoriteRounded /> : <FavoriteBorderRounded />}
        </IconButton>
      </div>

      {/* Playback controls */}
      <div className="buttons-and-progress">
        <IconButton onClick={playerActions.playPrevious}>
          <SkipPreviousRounded />
        </IconButton>
        <IconButton onClick={playerActions.togglePause}>
          {isLoading ? <CircularProgress /> :
           isPlaying ? <PauseRounded /> : <PlayArrowRounded />}
        </IconButton>
        <IconButton onClick={playerActions.playNext}>
          <SkipNextRounded />
        </IconButton>
        
        {/* Progress bar */}
        <Slider
          value={currentTime * 5}
          max={duration * 5}
          onChange={(_, value) => playerActions.seekTo(value / 5)}
        />
      </div>

      {/* Volume controls */}
      <div className="volume-and-others">
        <IconButton onClick={() => setMuted(!muted)}>
          <VolumeUpRounded />
        </IconButton>
        <Slider value={volume} onChange={(_, v) => setVolume(v)} />
        <IconButton onClick={playerActions.toggleQueue}>
          <QueueMusicRounded />
        </IconButton>
      </div>
    </div>
  );
}
Location: src/components/Player.jsx

Player Features

Playback Controls

Play, pause, next, previous with loading states

Progress Tracking

Real-time progress bar with seek functionality

Volume Control

Volume slider and mute toggle

Track Metadata

Album art, title, artist with like button

TrackList Component

Displays a list of tracks with play functionality:
import { useStore } from "@nanostores/react";
import { playerStore, playerActions } from "../stores/playerStore";

export default function TrackList({ tracks, hideImage, hideAlbum }) {
  const { currentTrack, isPlaying, isLoading } = useStore(playerStore);

  const playTrackItem = (track) => {
    playerActions.playTrack(track, tracks);
  };

  return (
    <Box>
      {/* Header row */}
      <Box className="track-list-header">
        <Box>#</Box>
        <Box>Title</Box>
        {!hideAlbum && <Box>Album</Box>}
        <Box>Duration</Box>
      </Box>

      {/* Track rows */}
      <List>
        {tracks.map((item, index) => (
          <ListItem onClick={() => playTrackItem(item)}>
            {/* Track index or equalizer */}
            <Box>
              {currentTrack?.trackId === item.trackId ? (
                isLoading ? <CircularProgress /> :
                isPlaying ? <EqualizerIcon /> :
                index + 1
              ) : (
                index + 1
              )}
            </Box>

            {/* Track info */}
            <Box sx={{ display: "flex" }}>
              {!hideImage && (
                <Avatar src={`${PROXY_URL}${item.thumbnailUrl}`} />
              )}
              <ListItemText
                primary={item.title}
                secondary={
                  item.artists.map((artist) => (
                    <Link to={`/artist/${artist.id}`}>
                      {artist.name}
                    </Link>
                  ))
                }
              />
            </Box>

            {/* Album link */}
            {!hideAlbum && (
              <Link to={`/album/${item.album.albumId}`}>
                {item.album.title}
              </Link>
            )}

            {/* Duration */}
            <Box>{item.duration.label}</Box>
          </ListItem>
        ))}
      </List>
    </Box>
  );
}
Location: src/components/TrackList.jsx

TrackList Features

  • Grid-based layout with proper column alignment
  • Active track highlighting with animated equalizer
  • Click-to-play functionality
  • Artist and album navigation links
  • Configurable display options (hideImage, hideAlbum)
  • Responsive design for mobile/desktop

QueueDrawer Component

Displays the playback queue in a collapsible drawer:
import { useStore } from "@nanostores/react";
import { playerStore, playerActions } from "../stores/playerStore";

function QueueDrawer() {
  const { isQueueOpen, queue, currentTrackIndex, queueDrawerWidth } = 
    useStore(playerStore);

  return (
    <Drawer
      anchor="right"
      variant="persistent"
      open={isQueueOpen}
      sx={{ width: isQueueOpen ? queueDrawerWidth : 0 }}
    >
      <Typography variant="h6">Queue</Typography>
      <List>
        {queue.map((track, index) => (
          <ListItem
            onClick={() => playerActions.playTrack(track)}
            sx={{
              backgroundColor:
                currentTrackIndex === index ? "action.selected" : "transparent",
              ...(currentTrackIndex === index && {
                borderLeft: "3px solid",
                borderImage: "linear-gradient(135deg, #7c3aed, #06b6d4) 1",
              }),
            }}
          >
            <ListItemText
              primary={track.title}
              secondary={track.artists[0]?.name}
            />
          </ListItem>
        ))}
      </List>
    </Drawer>
  );
}
Location: src/components/QueueDrawer.jsx

Queue Features

  • Persistent drawer that opens/closes via player toggle
  • Highlights currently playing track
  • Click any track to jump to it
  • Gradient border accent on active track
  • Hidden on mobile devices

Search Components

Search functionality is split across multiple specialized components:

Component Responsibilities

ComponentPurpose
SearchInputGlobal search bar in header, handles query submission
SearchTabsTab navigation between result types (All, Tracks, Albums, Artists)
SearchAllResultsCombined view showing all result types
SearchTracksResultsTrack-specific results with TrackList
SearchAlbumsResultsAlbum grid results
SearchArtistsResultsArtist card results
All search components consume searchStore to sync query and active tab state.
The main navigation sidebar with responsive mobile support:
function Sidebar({ mobileOpen, onMobileToggle }) {
  return (
    <>
      {/* Mobile drawer */}
      <Drawer
        variant="temporary"
        open={mobileOpen}
        onClose={onMobileToggle}
        sx={{ display: { xs: "block", sm: "none" } }}
      >
        <SidebarContent />
      </Drawer>

      {/* Desktop permanent drawer */}
      <Drawer
        variant="permanent"
        sx={{ display: { xs: "none", sm: "block" } }}
      >
        <SidebarContent />
      </Drawer>
    </>
  );
}

function SidebarContent() {
  return (
    <Box sx={{ width: SIDEBAR_WIDTH }}>
      <List>
        <ListItem component={Link} to="/">
          <ListItemIcon><HomeIcon /></ListItemIcon>
          <ListItemText primary="Home" />
        </ListItem>
        <ListItem component={Link} to="/explore">
          <ListItemIcon><ExploreIcon /></ListItemIcon>
          <ListItemText primary="Explore" />
        </ListItem>
        {/* More navigation items */}
      </List>
    </Box>
  );
}
Location: src/components/Sidebar.jsx

Component Communication Patterns

1. Store-based Communication

Components communicate via shared stores:
// TrackList updates store
function TrackList({ tracks }) {
  const playTrack = (track) => {
    playerActions.playTrack(track, tracks);
  };
}

// Player reacts to store changes
function Player() {
  const { currentTrack } = useStore(playerStore);
  return <div>{currentTrack?.title}</div>;
}

2. Props-based Communication

Parent-to-child data flow:
function AlbumPage() {
  const [tracks, setTracks] = useState([]);
  
  return <TrackList tracks={tracks} hideAlbum />;
}

3. Event-based Communication

Child-to-parent callbacks:
function App() {
  const [mobileOpen, setMobileOpen] = useState(false);
  
  return (
    <Sidebar 
      mobileOpen={mobileOpen} 
      onMobileToggle={() => setMobileOpen(!mobileOpen)} 
    />
  );
}

Styling Approach

Beat App uses Material-UI’s sx prop for component styling:
<Box
  sx={{
    display: "flex",
    flexDirection: "column",
    gap: 2,
    p: 3,
    bgcolor: "background.paper",
    borderRadius: 2,
    // Responsive styles
    px: { xs: 1.5, sm: 3 },
  }}
>
  {/* Content */}
</Box>

Styling Benefits

Theme Integration

Automatic light/dark mode support via theme tokens

Responsive Design

Breakpoint-based responsive values

Type Safety

TypeScript-aware style definitions

Performance

CSS-in-JS with optimized runtime

Shared Utilities

Audio Element

A singleton audio element shared across components:
// src/audioEl.js
const audioEl = new Audio();
export default audioEl;
Imported by:
  • playerStore.js - Event listeners and playback control
  • Player.jsx - Volume control sync

IndexedDB Utils

Local storage utilities for user data:
// src/lib/idb.js
export async function isLiked(trackId) { ... }
export async function toggleLike(track) { ... }
export async function addRecentPlay(track) { ... }
Used in:
  • Player.jsx - Like button functionality
  • LibraryPage.jsx - Display liked tracks

Component Best Practices

Each component should have one clear purpose:
  • Player handles playback controls
  • TrackList displays tracks
  • QueueDrawer shows the queue
Avoid creating “god components” that do everything.
Build complex UIs by composing simple components:
<PageLayout>
  <PageHeader title="Album" />
  <PageContent>
    <TrackList tracks={tracks} />
  </PageContent>
</PageLayout>
Prefer controlled components for better state management:
// Controlled - state managed by parent
<Slider value={volume} onChange={setVolume} />

// Uncontrolled - internal state
<Slider defaultValue={50} />
Use TypeScript or PropTypes to validate component props:
TrackList.propTypes = {
  tracks: PropTypes.arrayOf(PropTypes.object).isRequired,
  hideImage: PropTypes.bool,
  hideAlbum: PropTypes.bool,
};

Next Steps

Architecture Overview

Understand the overall system architecture

State Management

Deep dive into Nanostores implementation

Build docs developers (and LLMs) love