On this page
- Purpose
- The Giant Switch Statement Nightmare
- A Command Router with Component Mapping
- Understanding Separation of Concerns
- Prerequisites & Tooling
- Knowledge Base
- Environment
- High-Level Architecture
- Command Flow Diagram
- The Restaurant Menu
- The Three-Layer Architecture
- The Implementation
- Defining the Command History Type
- Defining Available Pages and External Links
- The Core Router Function
- Implementing Simple Commands
- Handling Commands with Arguments
- The Default Case (Command Not Found)
- Complete Implementation
- Under the Hood
- Memory & Performance
- The Switch Statement vs. Object Lookup
- Component Closure Behavior
- Edge Cases & Pitfalls
- Commands with Multiple Arguments
- Case Sensitivity
- Empty Input
- Special Characters
- Navigation Side Effects
- Component Import Overhead
- Conclusion
- Skills Acquired
- Extending the Router
Purpose
The Giant Switch Statement Nightmare
You’re building a terminal-style portfolio interface. Users can type commands like help, about, skills, cd projects, etc. Your first instinct might be to handle this in the component:
function Terminal() {
const handleCommand = (input: string) => {
if (input === "help") {
return <HelpComponent />;
} else if (input === "about") {
return <AboutComponent />;
} else if (input === "skills") {
return <SkillsComponent />;
} else if (input.startsWith("cd ")) {
const page = input.split(" ")[1];
// ... more logic
} else {
return <NotFoundComponent />;
}
};
}
This works for 3 commands. But what about 10? 20? The component becomes a 1000-line monolith where:
- Adding a new command requires editing the core component
- Testing individual commands is impossible without mounting the entire terminal
- Command logic is tightly coupled to UI rendering
- You can’t reuse command parsing logic elsewhere
The Core Problem: Mixing routing logic (which command to execute) with business logic (what the command does) and presentation logic (how to display results) violates the Single Responsibility Principle.
A Command Router with Component Mapping
The code we’re analyzing (src/lib/renderCmd.tsx) implements a Command Router Pattern that:
- Separates command parsing from execution
- Maps command strings to React components
- Handles arguments and validation
- Provides a single entry point for command execution
- Returns a standardized result object
This is the same pattern used by:
- Express.js (route handlers)
- React Router (URL to component mapping)
- CLI frameworks (argparse, commander.js)
Understanding Separation of Concerns
This tutorial demonstrates three architectural principles:
- Command Pattern: Encapsulating requests as objects
- Strategy Pattern: Selecting algorithms (components) at runtime
- Single Responsibility: Each command component does one thing
🔵 Deep Dive: The Command Pattern is one of the Gang of Four design patterns. It’s used in undo/redo systems, job queues, and macro recording—anywhere you need to decouple “what to do” from “when to do it.”
Prerequisites & Tooling
Knowledge Base
Required:
- JavaScript/TypeScript basics (functions, objects, arrays)
- React fundamentals (components, JSX)
- String manipulation (split, includes)
Helpful:
- Understanding of design patterns
- Familiarity with routing concepts
- Experience with switch statements
Environment
From the project structure:
// File structure
src/
lib/
renderCmd.tsx // Command router
components/
commands/
Help.tsx // Individual command components
About.tsx
Skills.tsx
Contact.tsx
Bio.tsx
Ls.tsx
NotFound.tsx
Key Concepts:
- Command: A string input from the user (e.g., “help”, “cd projects”)
- Component: A React component that renders the command’s output
- Router: The function that maps commands to components
High-Level Architecture
Command Flow Diagram
graph LR
A[User Input: 'cd projects'] --> B[renderCmd Function]
B --> C{Parse Command}
C --> D[Extract: command='cd', arg='projects']
D --> E{Switch Statement}
E --> F[Match 'cd' Case]
F --> G{Validate Argument}
G --> H[Check if 'projects' in pages array]
H --> I{Valid?}
I -->|Yes| J[Navigate to /projects]
I -->|No| K[Return Error Component]
J --> L[Return CmdHistory Object]
K --> L
L --> M[Terminal Displays Result]
style B fill:#a855f7
style E fill:#10b981
style G fill:#f59e0b
The Restaurant Menu
Think of the command router as a restaurant ordering system:
| Restaurant | Command Router |
|---|---|
| Menu items | Available commands |
| Order (e.g., “Burger with fries”) | User input (e.g., “cd projects”) |
| Kitchen stations | Command components |
| Waiter | renderCmd function |
| Prepared dish | CmdHistory object |
When you order “Burger with fries”:
- Waiter (renderCmd) takes your order
- Parses it: main item = “Burger”, side = “fries”
- Routes to the grill station (component)
- Returns the prepared dish (JSX)
The waiter doesn’t cook—they just route orders to the right kitchen station.
The Three-Layer Architecture
Layer 1: User Input (Unstructured)
└─ "cd projects" (string)
Layer 2: Command Router (Parsing & Routing)
└─ renderCmd.tsx
└─ Parses, validates, maps to component
Layer 3: Command Components (Execution)
└─ Individual components render output
The Implementation
Defining the Command History Type
Goal: Create a standardized return type for all commands.
type CmdHistory = {
cmd: string; // Original command string
Component: () => JSX.Element; // React component to render
time: string; // Timestamp (populated by caller)
};
Why This Structure?
cmd: Allows displaying what the user typed (echo)Component: Enables lazy rendering (component only renders when needed)time: Supports command history with timestamps
🔵 Deep Dive: Using Component: () => JSX.Element instead of Component: JSX.Element is crucial. The former is a function that returns JSX (can be called multiple times), while the latter is a value (rendered once and cached).
Defining Available Pages and External Links
Naive Approach: Hardcode URLs Everywhere
// WRONG: Scattered throughout the code
case "cd":
if (arg === "projects") window.location.href = "/projects";
if (arg === "github") window.open("https://github.com/...");
// ... repeated for every page
Why This Fails: Adding a new page requires editing multiple places. Changing a URL breaks everything.
Refined Solution (From Repo):
// Centralized configuration
const pages = [
"projects",
"tutorials",
"categories",
"tags",
"contact",
"about",
"github",
"linkedin",
];
const externalLinks: { [key: string]: string } = {
"github": "https://github.com/jason-n-tran",
"linkedin": "https://www.linkedin.com/in/jason-n-tran",
};
Benefits:
- Single source of truth for available pages
- Easy to add new pages (just add to array)
- External links separated from internal routes
- Can be imported and reused elsewhere (e.g., autocomplete)
The Core Router Function
The Function Signature:
function renderCmd(cmd: string): CmdHistory {
// Parse the command
const [command, arg] = cmd.split(" ");
// Route to appropriate handler
switch (command) {
// ... cases
}
}
🔴 Danger: The split(" ") approach only handles single arguments. For commands like cd my folder (with spaces), you’d need more sophisticated parsing. Production systems use libraries like yargs or commander.
Implementing Simple Commands
Pattern: Commands without arguments map directly to components.
switch (command) {
case "help":
return {
cmd,
Component: Help,
time: "",
};
case "whois":
return {
cmd,
Component: Bio,
time: "",
};
case "skills":
return {
cmd,
Component: Skills,
time: "",
};
case "contact":
return {
cmd,
Component: Contact,
time: "",
};
case "about":
return {
cmd,
Component: About,
time: "",
};
case "ls":
return {
cmd,
Component: Ls,
time: "",
};
Key Insight: Notice we’re returning the component function itself, not calling it. The terminal component will call it later:
// In the terminal component
<enteredCmd.Component /> // Calls the function
Handling Commands with Arguments
The Challenge: The cd command needs to:
- Validate the argument exists
- Distinguish between internal and external links
- Perform navigation
- Return appropriate feedback
Implementation:
case "cd": {
// Check if argument was provided and is valid
if (arg && pages.includes(arg)) {
// Check if it's an external link
if (externalLinks[arg]) {
window.open(externalLinks[arg], "_blank");
// Return a component that shows feedback
const Redirecting = () => <p>Opening {arg} in a new tab...</p>;
return {
cmd,
Component: Redirecting,
time: "",
};
} else {
// Internal navigation
window.location.href = `/${arg}`;
const Redirecting = () => <p>Redirecting to /{arg}...</p>;
return {
cmd,
Component: Redirecting,
time: "",
};
}
}
// Invalid argument - return error
return {
cmd,
Component: () => (
<p>Invalid directory. Type 'ls' to see available directories.</p>
),
time: "",
};
}
🔵 Deep Dive: Creating inline components (const Redirecting = () => <p>...</p>) is a powerful pattern for dynamic content. Each invocation creates a new component with closure over the arg variable.
The Default Case (Command Not Found)
default:
return {
cmd,
Component: NotFound,
time: "",
};
Why a Separate Component?
The NotFound component can:
- Display helpful error messages
- Suggest similar commands (fuzzy matching)
- Show usage examples
- Log analytics about failed commands
Complete Implementation
Here’s the full router from the repository:
import About from "@/components/commands/About";
import Bio from "@/components/commands/Bio";
import Contact from "@/components/commands/Contact";
import Help from "@/components/commands/Help";
import Ls from "@/components/commands/Ls";
import NotFound from "@/components/commands/NotFound";
import Skills from "@/components/commands/Skills";
type CmdHistory = {
cmd: string;
Component: () => JSX.Element;
time: string;
};
const pages = [
"projects",
"tutorials",
"categories",
"tags",
"contact",
"about",
"github",
"linkedin",
];
const externalLinks: { [key: string]: string } = {
"github": "https://github.com/jason-n-tran",
"linkedin": "https://www.linkedin.com/in/jason-n-tran",
};
function renderCmd(cmd: string): CmdHistory {
const [command, arg] = cmd.split(" ");
switch (command) {
case "help":
return { cmd, Component: Help, time: "" };
case "whois":
return { cmd, Component: Bio, time: "" };
case "skills":
return { cmd, Component: Skills, time: "" };
case "contact":
return { cmd, Component: Contact, time: "" };
case "about":
return { cmd, Component: About, time: "" };
case "ls":
return { cmd, Component: Ls, time: "" };
case "cd": {
if (arg && pages.includes(arg)) {
if (externalLinks[arg]) {
window.open(externalLinks[arg], "_blank");
const Redirecting = () => <p>Opening {arg} in a new tab...</p>;
return { cmd, Component: Redirecting, time: "" };
} else {
window.location.href = `/${arg}`;
const Redirecting = () => <p>Redirecting to /{arg}...</p>;
return { cmd, Component: Redirecting, time: "" };
}
}
return {
cmd,
Component: () => (
<p>Invalid directory. Type 'ls' to see available directories.</p>
),
time: "",
};
}
default:
return { cmd, Component: NotFound, time: "" };
}
}
export default renderCmd;
Under the Hood
Memory & Performance
When Does This Run?
The router executes synchronously when the user presses Enter:
User types "help" → Press Enter
↓
Terminal component calls renderCmd("help")
↓ (< 1ms)
Returns { cmd: "help", Component: Help, time: "" }
↓
Terminal adds to history state
↓
React re-renders with new history
↓
<Help /> component renders
Performance Characteristics:
- Time Complexity: O(1) for switch statement lookup
- Space Complexity: O(1) - only creates one CmdHistory object
- Render Cost: Depends on the command component (Help is ~50 lines of JSX)
Real-World Impact:
For a typical command:
- String parsing: ~0.01ms
- Switch lookup: ~0.001ms
- Component creation: ~0.1ms
- Total: ~0.111ms (imperceptible to users)
The Switch Statement vs. Object Lookup
Alternative Implementation: Object Map
const commandMap = {
help: Help,
whois: Bio,
skills: Skills,
contact: Contact,
about: About,
ls: Ls,
};
function renderCmd(cmd: string): CmdHistory {
const [command, arg] = cmd.split(" ");
const Component = commandMap[command] || NotFound;
return { cmd, Component, time: "" };
}
Comparison:
| Approach | Pros | Cons |
|---|---|---|
| Switch | Handles complex logic (cd case) | Verbose for simple cases |
| Object Map | Concise, easy to extend | Can’t handle arguments easily |
Hybrid Approach (Best of Both):
const simpleCommands = {
help: Help,
whois: Bio,
skills: Skills,
contact: Contact,
about: About,
ls: Ls,
};
function renderCmd(cmd: string): CmdHistory {
const [command, arg] = cmd.split(" ");
// Check simple commands first
if (simpleCommands[command]) {
return { cmd, Component: simpleCommands[command], time: "" };
}
// Handle complex commands
switch (command) {
case "cd":
// ... complex logic
default:
return { cmd, Component: NotFound, time: "" };
}
}
Component Closure Behavior
The Inline Component Pattern:
const Redirecting = () => <p>Opening {arg} in a new tab...</p>;
What’s Happening:
argis captured in the closure- Each call to
renderCmdcreates a newRedirectingfunction - The function “remembers” the
argvalue from its creation context
Memory Implications:
// Call 1: renderCmd("cd github")
const Redirecting1 = () => <p>Opening github in a new tab...</p>;
// Closure captures: arg = "github"
// Call 2: renderCmd("cd linkedin")
const Redirecting2 = () => <p>Opening linkedin in a new tab...</p>;
// Closure captures: arg = "linkedin"
// These are DIFFERENT functions with DIFFERENT closures
Redirecting1 !== Redirecting2 // true
This is safe because each command execution creates its own closure scope.
Edge Cases & Pitfalls
Commands with Multiple Arguments
Problem: cd my folder splits into ["cd", "my"], losing “folder”.
Current Behavior: Treats “my” as the argument, ignores “folder”.
Solution: Use regex or limit split:
const [command, ...args] = cmd.split(" ");
const arg = args.join(" "); // Rejoin remaining parts
// Or use regex
const match = cmd.match(/^(\w+)\s+(.+)$/);
if (match) {
const [, command, arg] = match;
}
Case Sensitivity
Problem: User types “HELP” instead of “help”.
Current Behavior: Falls through to default case (NotFound).
Solution: Normalize input:
const [command, arg] = cmd.toLowerCase().split(" ");
🔴 Danger: This breaks case-sensitive arguments. Better approach:
const parts = cmd.split(" ");
const command = parts[0].toLowerCase();
const arg = parts.slice(1).join(" "); // Preserve original case
Empty Input
Problem: User presses Enter without typing anything.
Current Behavior: command = "", falls to default case.
Better Approach: Handle explicitly:
function renderCmd(cmd: string): CmdHistory {
const trimmed = cmd.trim();
if (!trimmed) {
return {
cmd,
Component: () => <></>, // Empty component
time: "",
};
}
const [command, arg] = trimmed.split(" ");
// ... rest of logic
}
Special Characters
Problem: User types cd projects; rm -rf / (command injection attempt).
Current Behavior: Treats entire string as one command, fails validation.
Security Note: Since we’re only routing to predefined components (not executing shell commands), this is safe. But if you ever add eval() or similar, you’d need sanitization.
Navigation Side Effects
Problem: The cd case calls window.location.href, which is a side effect in a pure function.
case "cd": {
window.location.href = `/${arg}`; // Side effect!
return { cmd, Component: Redirecting, time: "" };
}
Why This Matters: Side effects make testing difficult. You can’t unit test this without mocking window.location.
Better Approach: Return navigation intent, let caller handle it:
type CmdHistory = {
cmd: string;
Component: () => JSX.Element;
time: string;
navigation?: { type: 'internal' | 'external', url: string };
};
case "cd": {
if (arg && pages.includes(arg)) {
if (externalLinks[arg]) {
return {
cmd,
Component: () => <p>Opening {arg}...</p>,
time: "",
navigation: { type: 'external', url: externalLinks[arg] }
};
}
}
}
// In terminal component
const result = renderCmd(cmd);
if (result.navigation) {
if (result.navigation.type === 'external') {
window.open(result.navigation.url, '_blank');
} else {
window.location.href = result.navigation.url;
}
}
Component Import Overhead
Problem: Importing all command components upfront increases bundle size.
import About from "@/components/commands/About";
import Bio from "@/components/commands/Bio";
// ... 10 more imports
Solution: Dynamic imports (code splitting):
const commandMap = {
help: () => import("@/components/commands/Help"),
about: () => import("@/components/commands/About"),
// ...
};
async function renderCmd(cmd: string): Promise<CmdHistory> {
const [command] = cmd.split(" ");
const loader = commandMap[command];
if (loader) {
const { default: Component } = await loader();
return { cmd, Component, time: "" };
}
return { cmd, Component: NotFound, time: "" };
}
This loads each command component only when first used.
Conclusion
Skills Acquired
You’ve learned:
- Command Pattern: Encapsulating requests as objects with standardized interfaces
- Routing Logic: Mapping string inputs to executable code
- Separation of Concerns: Decoupling parsing, validation, and execution
- Component Composition: Using functions as first-class values in React
- Configuration Management: Centralizing route definitions
The Proficiency Marker: Most developers hardcode command handling in components. You now understand command routing as a separate architectural layer that can be tested, extended, and reused independently. This mental model transfers to:
- API route handlers (Express, Fastify)
- State machines (XState)
- Event handlers (Redux actions)
- Plugin systems (VS Code extensions)
Extending the Router
Adding a New Command:
- Create the component:
// src/components/commands/Projects.tsx
export default function Projects() {
return <div>My projects...</div>;
}
- Import and add to router:
import Projects from "@/components/commands/Projects";
case "projects":
return { cmd, Component: Projects, time: "" };
- Update the
lscommand to show it:
// In Ls.tsx
const pages = [..., "projects"];
Adding Command Aliases:
case "help":
case "?":
case "h":
return { cmd, Component: Help, time: "" };
Adding Command Flags:
case "ls": {
const showHidden = arg === "-a" || arg === "--all";
const LsComponent = () => <Ls showHidden={showHidden} />;
return { cmd, Component: LsComponent, time: "" };
}
Next Challenge: Implement command autocomplete using a Trie data structure to suggest commands as users type, similar to bash/zsh completion.