Function Components in Vanilla JavaScript

Introduction

You've been updating the DOM by writing HTML strings directly in main.js:

document.querySelector("#app").innerHTML = `
  <h1 class="text-3xl font-bold underline">
    Hello Vite!
  </h1>
`;

This works fine for simple apps, but what happens when you need to display 20 product cards? Or show the same user profile in multiple places? Repeating code gets messy fast.

Function components solve this problem by wrapping UI patterns into reusable functions. You already know functions and template literals—this lesson just combines them in a practical way.

Before You Begin

Use your template repository for all practice exercises and the homework assignment. If you haven't set this up yet:

  1. Accept the GitHub Classroom assignment link from your instructor
  2. Clone the repository to your local machine
  3. Run npm install to install dependencies
  4. Start the dev server with npm run dev

Learning Objectives

By the end of this lesson, you'll be able to:

  • Write function components that return HTML strings
  • Understand component naming conventions (PascalCase vs camelCase)
  • Pass data into components using parameters
  • Compose multiple components together
  • Reduce code repetition in your projects
  • Understand how this prepares you for React development

Assignment Requirements

Use the GitHub Classroom repository created for this course to complete the following assignment

Note: To receive full credit for this lesson, you must:

  1. Complete all "Try it" and "Hands-On Practice" exercises in the lesson
  2. Provide screenshots showing your browser output for each exercise
  3. Use your own custom data/props (not the exact examples from the lesson)
    • Example: Instead of products with "Laptop" and "Mouse", use your own items
    • Instead of users named "Sarah" and "Mike", use different names/data
  4. Include commits with clear messages showing your progress
  5. Reference the 'Try it' and 'Hands-On Practice' sections in your reflection. How did they help you understand the concepts and to ultimately complete the homework assignment?

Why custom data? This proves you understand the concepts, not just copy-paste. Your screenshots will be unique to you.

What You Already Know

Functions that return values:

function createGreeting(name) {
  return `Hello, ${name}!`;
}

const message = createGreeting("Sarah"); // "Hello, Sarah!"

Template literals for HTML:

const html = `
  <div class="card">
    <h2>Product Name</h2>
    <p>$29.99</p>
  </div>
`;

Function components are just these two things combined:

function ProductCard() {
  return `
    <div class="card">
      <h2>Product Name</h2>
      <p>$29.99</p>
    </div>
  `;
}

Your First Component

Practice Workflow: For each exercise, you'll create separate practice files (practice-01.js, practice-02.js, etc.) to keep your work organized. You'll need to update the <script> tag in index.html to load each practice file as you work through them. The final homework assessment will use main.js.

Let's start with the simplest possible example—a heading component.

Your Vite template starts with this in index.html:

<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>

And this in src/main.js:

document.querySelector("#app").innerHTML = `
  <h1 class="text-3xl font-bold underline">
    Hello Vite!
  </h1>
`;

Let's convert this to a component:

function Heading() {
  return `
    <h1 class="text-3xl font-bold underline">
      Hello Vite!
    </h1>
  `;
}

document.querySelector("#app").innerHTML = Heading();

What changed?

  • Wrapped the HTML in a function called Heading
  • Called Heading() to get the HTML string
  • Result is identical, but now the heading is reusable

Try It 1: Your First Component

In your template repository, create a new file src/practice-01.js and add the Heading() component code above. Update your index.html to load this file instead of main.js:

<script type="module" src="/src/practice-01.js"></script>

Verify it works the same as the original. Take a screenshot of your browser showing the result.

Naming Convention: PascalCase vs camelCase

Important convention: Component functions use PascalCase (capital first letter).

// ✅ Component functions - PascalCase
function UserCard() {}
function ProductList() {}
function NavigationBar() {}

// ✅ Regular functions - camelCase
function calculateTotal() {}
function getUserData() {}
function formatDate() {}

Why the difference?

  • PascalCase signals "this returns UI/markup"
  • camelCase signals "this does logic/calculations"
  • This convention comes from React and is widely adopted in the JavaScript community

Checkpoint Question: Which naming style would you use for a function that renders a button? What about a function that validates an email address?

Adding Parameters (The "Props" Concept)

The real power comes when you pass data into components:

function Greeting(name) {
  return `<p class="text-2xl font-bold">Hello, ${name}!</p>`;
}

document.querySelector("#app").innerHTML = `
  ${Greeting("Sarah")}
  ${Greeting("Mike")}
  ${Greeting("Jessica")}
`;

Output:

<p class="text-2xl font-bold">Hello, Sarah!</p>
<p class="text-2xl font-bold">Hello, Mike!</p>
<p class="text-2xl font-bold">Hello, Jessica!</p>

Same component, different data—no repetition!

Try It 2: Multiple Greetings

Create src/practice-02.js with the Greeting() component code. Update index.html to load this file. Test it with your own custom names (not Sarah, Mike, or Jessica). Take a screenshot.

Hands-On Practice 1: Badge Component

Create src/practice-03.js with a Badge component that takes a status parameter and returns different colored badges:

function Badge(status) {
  // Your code here
  // "success" should be green background (bg-green-500)
  // "warning" should be yellow background (bg-yellow-500)
  // "error" should be red background (bg-red-500)
  // All badges should have white text and padding
}

// Test it:
document.querySelector("#app").innerHTML = `
  ${Badge("success")}
  ${Badge("warning")}
  ${Badge("error")}
`;

Hint: Use an if/else statement or ternary operators to choose the right color class based on the status parameter. Update index.html to load practice-03.js. Take a screenshot of your three badges.

Real-World Example: Product Card

Let's build something practical:

function ProductCard(name, price, inStock) {
  const stockClass = inStock ? "text-green-600" : "text-red-600";
  const stockText = inStock ? "In Stock" : "Out of Stock";

  return `
    <div class="rounded-lg border border-gray-200 p-4">
      <h3 class="text-lg font-semibold">${name}</h3>
      <p class="text-xl font-bold text-gray-900">$${price}</p>
      <p class="${stockClass}">${stockText}</p>
      <button class="mt-2 rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
        Add to Cart
      </button>
    </div>
  `;
}

// Use it
document.querySelector("#app").innerHTML = `
  <div class="grid grid-cols-3 gap-4 p-8">
    ${ProductCard("Laptop", 999, true)}
    ${ProductCard("Mouse", 29, true)}
    ${ProductCard("Keyboard", 79, false)}
  </div>
`;

Benefits:

  • Write the card layout once
  • Reuse it with different data
  • Easy to maintain (change one function, all cards update)
  • Clean, readable code

Try It 3: Product Cards

Create src/practice-04.js with the ProductCard() component. Change the product data to your own items (not Laptop, Mouse, Keyboard). Update index.html. Take a screenshot of your three custom product cards.

Using Objects for Cleaner Parameters

Multiple parameters get messy. Use objects instead:

function ProductCard(product) {
  return `
    <div class="rounded-lg border p-4">
      <h3 class="font-semibold">${product.name}</h3>
      <p class="text-xl">$${product.price}</p>
      <p class="${product.inStock ? "text-green-600" : "text-red-600"}">
        ${product.inStock ? "In Stock" : "Out of Stock"}
      </p>
    </div>
  `;
}

// Much cleaner to call
const laptop = { name: "Laptop", price: 999, inStock: true };
const mouse = { name: "Mouse", price: 29, inStock: true };

document.querySelector("#app").innerHTML = `
  <div class="grid grid-cols-2 gap-4 p-8">
    ${ProductCard(laptop)}
    ${ProductCard(mouse)}
  </div>
`;

Why objects are better:

  • Named properties are clearer than positional arguments
  • Easy to add new properties without breaking existing code
  • Matches how data comes from APIs and databases

Composing Components

Components can call other components:

function PriceTag(price) {
  return `<span class="text-xl font-bold text-gray-900">$${price}</span>`;
}

function StockBadge(inStock) {
  const color = inStock
    ? "bg-green-100 text-green-800"
    : "bg-red-100 text-red-800";
  const text = inStock ? "In Stock" : "Out of Stock";
  return `<span class="rounded px-2 py-1 text-sm ${color}">${text}</span>`;
}

function ProductCard(product) {
  return `
    <div class="rounded-lg border p-4">
      <h3 class="font-semibold">${product.name}</h3>
      ${PriceTag(product.price)}
      ${StockBadge(product.inStock)}
    </div>
  `;
}

Benefits:

  • Break complex UI into smaller pieces
  • Each component has one job
  • Easy to test and debug
  • Reuse small components in different contexts

Checkpoint Question: If you needed to use StockBadge in a shopping cart component, would you need to rewrite it? Why or why not? Answer in your reflection.

Try It 4: Composing Components

Create src/practice-05.js with the composed ProductCard, PriceTag, and StockBadge components shown above. Test it with your own product data. Update index.html. Take a screenshot.

Mapping Arrays to Components

Combine with map() to render lists:

const products = [
  { name: "Laptop", price: 999, inStock: true },
  { name: "Mouse", price: 29, inStock: true },
  { name: "Keyboard", price: 79, inStock: false },
];

document.querySelector("#app").innerHTML = `
  <div class="grid grid-cols-3 gap-4 p-8">
    ${products.map((product) => ProductCard(product)).join("")}
  </div>
`;

What's happening:

  1. map() transforms each product object into HTML using ProductCard()
  2. Returns an array of HTML strings: ["<div>...</div>", "<div>...</div>", "<div>...</div>"]
  3. join('') concatenates them into one big string
  4. Result: all products rendered without repetition

Why join('') is necessary:

Without it, you'd see commas between cards:

// Without join
products.map((product) => ProductCard(product));
// ["<div>Card 1</div>", "<div>Card 2</div>"]
// Renders as: <div>Card 1</div>,<div>Card 2</div>

// With join('')
products.map((product) => ProductCard(product)).join("");
// "<div>Card 1</div><div>Card 2</div>"
// Renders cleanly with no commas

Practical Exercise: User Profile Components

Build these components step by step:

// Step 1: Build individual pieces
function Avatar(imageUrl) {
  return `<img src="${imageUrl}" class="h-16 w-16 rounded-full" alt="User avatar" />`;
}

function UserName(name) {
  return `<h2 class="text-xl font-bold">${name}</h2>`;
}

function UserBio(bio) {
  return `<p class="text-gray-600">${bio}</p>`;
}

// Step 2: Compose them together
function UserProfile(user) {
  return `
    <div class="flex items-center gap-4 rounded-lg border p-4">
      ${Avatar(user.avatar)}
      <div>
        ${UserName(user.name)}
        ${UserBio(user.bio)}
      </div>
    </div>
  `;
}

// Step 3: Render multiple users
const users = [
  {
    name: "Sarah",
    bio: "Web Developer",
    avatar: "https://i.pravatar.cc/150?img=1",
  },
  { name: "Mike", bio: "Designer", avatar: "https://i.pravatar.cc/150?img=2" },
  {
    name: "Jessica",
    bio: "Product Manager",
    avatar: "https://i.pravatar.cc/150?img=3",
  },
];

document.querySelector("#app").innerHTML = `
  <div class="space-y-4 p-8">
    ${users.map((user) => UserProfile(user)).join("")}
  </div>
`;

Hands-On Practice 2: User Profile Components

Create src/practice-06.js with the user profile components (Avatar, UserName, UserBio, UserProfile). Change the user data to your own people (different names, bios, and avatars). Update index.html. Take a screenshot of your three custom user profiles.

When to Use Components

Good use cases:

  • Repeated UI patterns (cards, buttons, badges)
  • Lists of similar items (products, users, posts)
  • Complex layouts that need organization
  • Any markup you use more than once

When NOT to use:

  • One-off elements that appear once
  • Very simple markup (single <p> tag)
  • When it makes code harder to read

Rule of thumb: If you copy-paste HTML, make it a component instead.

Components vs Manual DOM Manipulation

Components (what we just learned):

function Counter(count) {
  return `<p>Count: ${count}</p>`;
}
// Replace entire innerHTML each time
document.querySelector("#app").innerHTML = Counter(5);

Manual DOM (what you learned earlier):

const countElement = document.querySelector("#count");
countElement.textContent = 5; // Update just this element

Use components for: Initial page render, static content, lists of items

Use manual DOM for: Updating specific elements, handling events, dynamic changes

You'll often combine both approaches:

  1. Use a component function to render the initial HTML
  2. Then use manual DOM to update specific parts when things change

Here's a simple example:

// Component renders the initial counter UI
function Counter(count) {
  return `
    <div>
      <button id="increment" class="rounded bg-blue-500 px-4 py-2 text-white">+</button>
      <p id="count">${count}</p>
      <button id="decrement" class="rounded bg-red-500 px-4 py-2 text-white">-</button>
    </div>
  `;
}

// Render once with component
let count = 0;
document.querySelector("#app").innerHTML = Counter(count);

// Update just the count text when buttons clicked (manual DOM)
document.querySelector("#increment").addEventListener("click", () => {
  count++;
  document.querySelector("#count").textContent = count;
});

document.querySelector("#decrement").addEventListener("click", () => {
  count--;
  document.querySelector("#count").textContent = count;
});

Why this works well:

  • Component creates clean initial HTML structure
  • Manual DOM updates are fast for small changes
  • No need to re-render everything on each click

Organizing Components in Separate Files

As your app grows, keep components in separate files. Here's how to set it up step-by-step:

Step 1: Create the Components Folder

In your project, create a new folder called components inside the src folder:

src/
├── components/    ← Create this folder
├── main.js

Step 2: Create a Component File

Inside src/components/, create a new file called ProductCard.js:

src/components/ProductCard.js:

export function ProductCard(product) {
  return `
    <div class="rounded-lg border p-4">
      <h3 class="font-semibold">${product.name}</h3>
      <p class="text-xl">$${product.price}</p>
    </div>
  `;
}

Notice the export keyword - this makes the function available to other files.

Step 3: Import and Use It

In src/main.js, import the component:

src/main.js:

import { ProductCard } from "./components/ProductCard.js";

const products = [
  { name: "Laptop", price: 999 },
  { name: "Mouse", price: 29 },
];

document.querySelector("#app").innerHTML = `
  <div class="grid gap-4 p-8">
    ${products.map(ProductCard).join("")}
  </div>
`;

Notice the import statement - this brings in the function from the other file.

Benefits

  • Each component in its own file
  • Easy to find and maintain
  • Can reuse across different pages
  • Professional project structure

Try It 5: Separate Files

Create a src/components/Badge.js file with your Badge component from Hands-On Practice 1. Export it, then create src/practice-07.js that imports and uses it. Update index.html to load practice-07.js. Take a screenshot showing it works.

Common Mistakes and How to Fix Them

Mistake 1: Forgetting to Call the Function

// ❌ Wrong - passing the function itself
document.querySelector("#app").innerHTML = ProductCard;
// Output: [Function: ProductCard]

// ✅ Correct - calling the function
document.querySelector("#app").innerHTML = ProductCard(product);

Mistake 2: Missing join('') with map

// ❌ Wrong - commas appear between items
products.map((product) => ProductCard(product));

// ✅ Correct - clean output
products.map((product) => ProductCard(product)).join("");

Mistake 3: Not Using Template Literals

// ❌ Wrong - regular string
function Heading() {
  return "<h1>Hello</h1>";
}

// ✅ Correct - template literal for readability
function Heading() {
  return `
    <h1 class="text-2xl">
      Hello
    </h1>
  `;
}

Homework: Build a Movie Card Component Library

This is your graded assessment. Create a movie browsing interface using function components in your template repository.

File Structure

For this assessment, use main.js (not practice files). Set up your project like this:

src/
├── components/
│   ├── MovieCard.js
│   ├── RatingBadge.js
│   └── MovieList.js
└── main.js

Requirements

  1. Create these components (each in its own file):

    RatingBadge.js - Takes a rating number, returns colored badge:

    • Green (bg-green-500) for ≥8
    • Yellow (bg-yellow-500) for ≥6
    • Red (bg-red-500) for less than 6

    MovieCard.js - Takes a movie object, displays:

    • Movie poster image
    • Title
    • Year
    • Rating badge (use your RatingBadge component)

    MovieList.js - Takes an array of movies:

    • Uses map() and join('') to render MovieCards
    • Displays in a grid layout
  2. Data to use in main.js:

const movies = [
  {
    title: "The Shawshank Redemption",
    year: 1994,
    rating: 9.3,
    poster: "https://via.placeholder.com/200x300",
  },
  {
    title: "The Godfather",
    year: 1972,
    rating: 9.2,
    poster: "https://via.placeholder.com/200x300",
  },
  {
    title: "The Dark Knight",
    year: 2008,
    rating: 9.0,
    poster: "https://via.placeholder.com/200x300",
  },
  {
    title: "Forrest Gump",
    year: 1994,
    rating: 8.8,
    poster: "https://via.placeholder.com/200x300",
  },
  {
    title: "Inception",
    year: 2010,
    rating: 8.8,
    poster: "https://via.placeholder.com/200x300",
  },
];
  1. Styling requirements:

    • Use Tailwind classes
    • Cards should have hover effects (hover:shadow-lg or similar)
    • Grid layout with 3 columns
  2. Technical requirements:

    • All component functions use PascalCase
    • Each component in its own file with export
    • Import all components in main.js
    • Use map() and join('') to render the list
    • Remember to use your own movie data (not the example movies)
  3. In main.js:

    • Import all three components
    • Define your movie array (use your own movies, not the examples)
    • Render the MovieList component
  4. Make sure index.html loads main.js:

    <script type="module" src="/src/main.js"></script>
    

Deliverable: Push to GitHub with commits showing your progress. Submit repo link with screenshots of your movie cards.

Reflection Questions

Answer these in a function-components-reflection.md file:

  1. Before this lesson, how did you handle repeating UI elements? What problems did you encounter?

  2. Explain in your own words: Why do component functions use PascalCase instead of camelCase?

  3. Give a real example from your homework where breaking UI into smaller components made the code easier to work with.

  4. What's the trickiest part of using map() and join('') together? Did you forget join('') at first?

  5. How does this relate to what you already knew? Connect function components to concepts you learned earlier (functions, template literals, arrays).

Looking Ahead: This IS React (Almost)

Here's a secret: What you just learned is 90% of how React works.

Vanilla JS component:

function Greeting(name) {
  return `<h1>Hello, ${name}!</h1>`;
}

React component:

function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

The differences:

  • React uses JSX instead of template literals (looks like HTML, not strings)
  • React handles re-rendering automatically
  • React has built-in state management

The similarities:

  • Both are functions that return UI
  • Both use PascalCase
  • Both accept parameters (props)
  • Both compose together
  • Same mental model!

You're already thinking like a React developer. When we get to React, it'll feel familiar—just with better tools.

Key Takeaways

  1. Function components = functions that return HTML strings
  2. PascalCase for components, camelCase for regular functions
  3. Parameters let you pass data into components (like props in React)
  4. Composition = components calling other components
  5. Combine with map() to render lists from arrays
  6. Always use join('') after map() to avoid commas
  7. Reduces repetition and makes code more maintainable
  8. This pattern is fundamental to modern JavaScript frameworks

Congratulations! You just learned one of the most important patterns in modern web development. This isn't just "vanilla JavaScript"—it's the foundation that React, Vue, and every modern framework is built on.