Components, Props & JSX for Themes
Master React function components, typed props, template decomposition, and the static-first mental model in ForgeWP.
If you are accustomed to traditional WordPress theme development, your mental model of a theme is likely centered around PHP template files: header.php, footer.php, and reusable fragments pulled in with get_template_part('template-parts/content', 'card').
In ForgeWP, React function components replace PHP template files and template parts. Instead of mixing imperative PHP loops (have_posts(), the_post()) with HTML tags, you compose declarative, type-safe React components that describe your user interface.
No Node.js Runtime in Production
Although you author theme UI with React, ForgeWP renders the app during export and generates the WordPress theme files. The production site does not need Node.js, but the theme can still include browser JavaScript for interactive islands and blocks.
The Mental Shift: PHP Template Parts vs React Components
In classic WordPress themes, creating a reusable blog card requires creating an isolated PHP file and calling WordPress template functions:
1<article id="post-<?php the_ID(); ?>" <?php post_class('card'); ?>>
2 <?php if (has_post_thumbnail()) : ?>
3 <div class="card-thumbnail">
4 <?php the_post_thumbnail('medium'); ?>
5 </div>
6 <?php endif; ?>
7 <div class="card-body">
8 <h2 class="card-title">
9 <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
10 </h2>
11 <p class="card-excerpt"><?php echo get_the_excerpt(); ?></p>
12 </div>
13</article>In ForgeWP, this becomes a typed, composable React function component:
1interface PostCardProps {
2 title: string
3 excerpt: string
4 permalink: string
5 featuredImage?: string
6 category?: string
7}
8
9export function PostCard({
10 title,
11 excerpt,
12 permalink,
13 featuredImage,
14 category,
15}: PostCardProps) {
16 return (
17 <article className="group flex flex-col overflow-hidden border border-zinc-200 bg-white transition hover:border-zinc-950">
18 {featuredImage && (
19 <div className="aspect-video w-full overflow-hidden bg-zinc-100">
20 <img
21 src={featuredImage}
22 alt={title}
23 className="h-full w-full object-cover transition duration-300 group-hover:scale-105"
24 loading="lazy"
25 />
26 </div>
27 )}
28 <div className="flex flex-1 flex-col p-6">
29 {category && (
30 <span className="mb-2 text-xs font-semibold tracking-wider text-zinc-500 uppercase">
31 {category}
32 </span>
33 )}
34 <h2 className="text-xl font-bold tracking-tight text-zinc-900">
35 <a href={permalink} className="hover:underline">
36 {title}
37 </a>
38 </h2>
39 <p className="mt-3 flex-1 text-sm leading-relaxed text-zinc-600">
40 {excerpt}
41 </p>
42 <div className="mt-6 border-t border-zinc-100 pt-4">
43 <a
44 href={permalink}
45 className="text-xs font-semibold text-zinc-900 underline underline-offset-4 hover:text-zinc-600"
46 >
47 Read Article →
48 </a>
49 </div>
50 </div>
51 </article>
52 )
53}The benefits of the React approach are immediate:
- Explicit Inputs (Props): You know exactly what data the component needs to render.
- Compile-Time Type Safety: TypeScript alerts you before compile time if a required property is missing or typed incorrectly.
- Local Encapsulation: Layout, markup, and styling live together in a single coherent unit.
Anatomy of a React Function Component
A React component is simply a JavaScript or TypeScript function that accepts an input object called props and returns JSX (markup):
1interface BadgeProps {
2 label: string
3 variant?: "default" | "outline" | "accent"
4}
5
6export function Badge({ label, variant = "default" }: BadgeProps) {
7 const styles = {
8 default: "bg-zinc-900 text-white",
9 outline: "border border-zinc-300 text-zinc-700",
10 accent: "bg-amber-100 text-amber-900 border border-amber-200",
11 }
12
13 return (
14 <span
15 className={`inline-flex items-center px-2.5 py-0.5 text-xs font-medium ${styles[variant]}`}
16 >
17 {label}
18 </span>
19 )
20}To render this component inside another file, import it and invoke it like an HTML element:
1import { Badge } from "./Badge"
2
3export function ArticleHeader() {
4 return (
5 <header className="border-b border-zinc-200 py-12">
6 <Badge label="Engineering" variant="accent" />
7 <h1 className="mt-4 text-4xl font-black tracking-tight text-zinc-950">
8 How We Built a Sub-Millisecond WordPress Theme
9 </h1>
10 </header>
11 )
12}Essential JSX Rules for Theme Developers
JSX is a syntax extension for JavaScript that looks like HTML. While intuitive, it has a few crucial rules that differ from plain HTML:
1. className instead of class
Because class is a reserved keyword in JavaScript, HTML's class attribute is written as className:
1// Correct:
2<div className="max-w-7xl mx-auto px-6 py-12">...</div>
3
4// Incorrect:
5<div class="max-w-7xl mx-auto px-6 py-12">...</div>Why do we use `className` instead of HTML's `class` attribute?
In React JSX, use the className prop to set an HTML element's CSS class. It is the React spelling for the DOM class attribute. TypeScript usually catches an unsupported class prop on typed JSX elements, but the exact diagnostic can depend on the element and project configuration.
2. Every Tag Must Be Closed
In standard HTML, tags like <img>, <input>, <br>, and <hr> do not strictly require a closing tag. In JSX, all tags must be explicitly closed or self-closed:
1// Always self-close void elements:
2<img src="/logo.svg" alt="ForgeWP" className="h-8 w-auto" />
3<hr className="my-8 border-zinc-200" />
4<input type="search" placeholder="Search articles..." className="px-4 py-2 border" />3. Single Root Element or Fragment (<>...</>)
A React component must return a single top-level element. If you need to return multiple sibling elements without introducing an unnecessary wrapper <div> to your markup, use a Fragment (<>...</>):
1// Valid: Fragment wraps siblings without rendering extra DOM nodes
2export function PostMeta({ author, date }: { author: string; date: string }) {
3 return (
4 <>
5 <span className="font-medium text-zinc-900">{author}</span>
6 <span className="text-zinc-400">•</span>
7 <time className="text-zinc-600">{date}</time>
8 </>
9 )
10}4. JavaScript Expressions in Curly Braces ({})
Any valid JavaScript expression can be embedded inside JSX using curly braces {}:
1export function Greeting({
2 username,
3 unreadCount,
4}: {
5 username: string
6 unreadCount: number
7}) {
8 return (
9 <div className="border bg-zinc-50 p-4">
10 <p className="text-sm text-zinc-700">
11 Welcome back,{" "}
12 <strong className="text-zinc-900">{username.toUpperCase()}</strong>!
13 </p>
14 {unreadCount > 0 ? (
15 <span className="text-xs text-emerald-600">
16 You have {unreadCount} new notifications
17 </span>
18 ) : (
19 <span className="text-xs text-zinc-400">All caught up</span>
20 )}
21 </div>
22 )
23}Decomposing Monolithic Templates into Atomic Components
One of the greatest advantages of React in WordPress theme engineering is atomic component architecture. Rather than writing a massive 500-line index.php or page.tsx, you break your UI into isolated, easily testable components.
Here is how a complete theme layout decomposes into clean layers:
src/app/page.tsx (Front Page Template)
-> <SiteHeader />
* <Logo />
* <PrimaryNavigation />
* <SearchButton />
-> <HeroSection />
* <Badge />
* <CallToAction />
-> <FeaturedPostsGrid />
* <PostCard /> (repeated per article)
-> <NewsletterSignup />
-> <SiteFooter />
* <FooterNavigation />
* <CopyrightNotice />Example: Rendering Lists of Content
When rendering collections of posts, pages, or navigation items, use JavaScript's native .map() method. Always supply a unique key prop (such as the post ID) to each item in the list:
1import { PostCard } from "./PostCard"
2
3interface PostSummary {
4 id: number
5 title: string
6 excerpt: string
7 slug: string
8 featuredImage?: string
9 categoryName?: string
10}
11
12interface FeaturedPostsGridProps {
13 heading: string
14 subheading?: string
15 posts: PostSummary[]
16}
17
18export function FeaturedPostsGrid({
19 heading,
20 subheading,
21 posts,
22}: FeaturedPostsGridProps) {
23 if (posts.length === 0) {
24 return (
25 <div className="rounded-lg border border-dashed border-zinc-300 p-12 text-center text-zinc-500">
26 No articles published yet. Check back soon!
27 </div>
28 )
29 }
30
31 return (
32 <section className="mx-auto max-w-7xl px-6 py-16">
33 <div className="mb-10 text-center">
34 <h2 className="text-3xl font-bold tracking-tight text-zinc-950 sm:text-4xl">
35 {heading}
36 </h2>
37 {subheading && (
38 <p className="mt-3 text-lg text-zinc-600">{subheading}</p>
39 )}
40 </div>
41
42 <div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
43 {posts.map((post) => (
44 <PostCard
45 key={post.id}
46 title={post.title}
47 excerpt={post.excerpt}
48 permalink={`/blog/${post.slug}`}
49 featuredImage={post.featuredImage}
50 category={post.categoryName}
51 />
52 ))}
53 </div>
54 </section>
55 )
56}Component Children and Slot Composition
Often, a component serves as a layout shell or container whose inner content varies depending on where it is used. In React, this is handled using the children prop:
1import React from "react"
2
3interface CardShellProps {
4 title?: string
5 children: React.ReactNode
6 footer?: React.ReactNode
7}
8
9export function CardShell({ title, children, footer }: CardShellProps) {
10 return (
11 <div className="overflow-hidden border border-zinc-200 bg-white">
12 {title && (
13 <div className="border-b border-zinc-100 bg-zinc-50/50 px-6 py-4">
14 <h3 className="font-semibold text-zinc-900">{title}</h3>
15 </div>
16 )}
17 <div className="p-6">{children}</div>
18 {footer && (
19 <div className="border-t border-zinc-100 bg-zinc-50 px-6 py-3">
20 {footer}
21 </div>
22 )}
23 </div>
24 )
25}You can now wrap arbitrary content inside CardShell:
1<CardShell
2 title="Author Profile"
3 footer={
4 <a
5 href="/author/sarah"
6 className="text-xs text-zinc-600 hover:text-zinc-950"
7 >
8 View all posts →
9 </a>
10 }
11>
12 <p className="text-sm text-zinc-600">
13 Sarah is a design engineer specializing in design systems and WordPress
14 theme architecture.
15 </p>
16</CardShell>The Static-First Mental Model
In classic web app frameworks (SPA mode), every React component runs on the client inside the user's browser, incurring hefty JavaScript bundle downloads, parsing delays, and hydration latency.
In ForgeWP:
Feature Single Page App (SPA) ForgeWP Theme Output Varies by application and rendering mode Rendered HTML plus WordPress PHP templates Client JavaScript Varies by application Added for interactive islands and blocks Search Engines (SEO) Depends on rendering and metadata Initial markup is present in exported output Hosting Requirements Depends on the app architecture WordPress hosting; no Node.js server needed |
|---|
ForgeWP renders the app during export. The compiler can leave static markup in the generated theme and discover eligible interactive components for client hydration. Use the documented source patterns for automatic splitting and explicit hydration boundaries when you need more control. See State, Interactivity & Auto-Hydration for how to recognize and test interactive islands.
Next Step: Utility-First Styling
Proceed to Styling with Tailwind CSS 4 to learn how utility classes connect directly to your theme's design system and WordPress presets.