Skip to main content
Back to Blog
Tutorial

How to Build a React FAQ Component (With API Integration)

Build a production-ready React FAQ component with accordion UI, search, and API-powered content. Includes TypeScript, accessibility, and SEO markup.

TheFAQApp TeamApril 6, 20268 min read

Why Build a React FAQ Component?

Every product needs a FAQ page. Hard-coding questions and answers directly into JSX works for five questions — but it breaks down fast. Content changes require code deploys, there's no search, and you lose SEO benefits like FAQ schema markup.

A better approach: build a reusable React FAQ component that pulls content from an API. Content updates happen instantly, your FAQ grows without code changes, and you get structured data for Google rich results.

This tutorial walks you through building a production-ready FAQ component in React with TypeScript — from a basic accordion to a searchable, API-powered FAQ page.

Step 1: Basic FAQ Accordion

Start with a simple accordion component that accepts an array of questions and answers:

// components/FAQItem.tsx
import { useState } from "react";

interface FAQItemProps {
  question: string;
  answer: string;
  defaultOpen?: boolean;
}

export function FAQItem({ question, answer, defaultOpen = false }: FAQItemProps) {
  const [isOpen, setIsOpen] = useState(defaultOpen);

  return (
    <div className="faq-item">
      <button
        className="faq-question"
        onClick={() => setIsOpen(!isOpen)}
        aria-expanded={isOpen}
      >
        <span>{question}</span>
        <span className="faq-icon">{isOpen ? "−" : "+"}</span>
      </button>
      {isOpen && (
        <div className="faq-answer" role="region">
          <p>{answer}</p>
        </div>
      )}
    </div>
  );
}
// components/FAQ.tsx
import { FAQItem } from "./FAQItem";

interface FAQEntry {
  question: string;
  answer: string;
}

interface FAQProps {
  items: FAQEntry[];
  title?: string;
}

export function FAQ({ items, title = "Frequently Asked Questions" }: FAQProps) {
  return (
    <section className="faq-section">
      <h2>{title}</h2>
      <div className="faq-list">
        {items.map((item, index) => (
          <FAQItem key={index} question={item.question} answer={item.answer} />
        ))}
      </div>
    </section>
  );
}

Usage:

const faqs = [
  { question: "What is your refund policy?", answer: "Full refund within 30 days." },
  { question: "How do I cancel?", answer: "Go to Settings → Billing → Cancel." },
];

<FAQ items={faqs} />

This works, but the content is hard-coded. Let's fix that.

Step 2: Fetch FAQ Content From an API

Instead of embedding content in your codebase, pull it from an API. This lets your team update FAQ content without deploying code.

Here's how to connect to the thefaq.app API using the @faqapp/core SDK:

npm install @faqapp/core
// hooks/useFAQ.ts
import { useState, useEffect } from "react";
import { FAQClient } from "@faqapp/core";

const client = new FAQClient({
  apiKey: import.meta.env.VITE_FAQ_API_KEY,
  organizationSlug: "your-org",
});

interface FAQQuestion {
  id: string;
  slug: string;
  question: string;
  answer: string;
  category?: string;
}

export function useFAQ(category?: string) {
  const [questions, setQuestions] = useState<FAQQuestion[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    async function fetchFAQs() {
      try {
        setLoading(true);
        const response = await client.questions.list({
          category,
          limit: 50,
        });
        setQuestions(response.data);
      } catch (err) {
        setError(err instanceof Error ? err : new Error("Failed to load FAQs"));
      } finally {
        setLoading(false);
      }
    }

    fetchFAQs();
  }, [category]);

  return { questions, loading, error };
}

Now update the FAQ component to use this hook:

// components/FAQ.tsx
import { FAQItem } from "./FAQItem";
import { useFAQ } from "../hooks/useFAQ";

interface FAQProps {
  category?: string;
  title?: string;
}

export function FAQ({ category, title = "Frequently Asked Questions" }: FAQProps) {
  const { questions, loading, error } = useFAQ(category);

  if (loading) return <div className="faq-loading">Loading FAQ...</div>;
  if (error) return <div className="faq-error">Unable to load FAQ content.</div>;
  if (questions.length === 0) return null;

  return (
    <section className="faq-section">
      <h2>{title}</h2>
      <div className="faq-list">
        {questions.map((item) => (
          <FAQItem key={item.id} question={item.question} answer={item.answer} />
        ))}
      </div>
    </section>
  );
}

Now your FAQ component is powered by an API. Update content in your thefaq.app dashboard and it appears instantly — no redeploy needed.

Step 3: Add Search

For FAQ pages with more than 10 questions, search is essential. The thefaq.app API includes full-text search:

// components/FAQWithSearch.tsx
import { useState, useEffect } from "react";
import { FAQClient } from "@faqapp/core";
import { FAQItem } from "./FAQItem";

const client = new FAQClient({
  apiKey: import.meta.env.VITE_FAQ_API_KEY,
  organizationSlug: "your-org",
});

export function FAQWithSearch() {
  const [query, setQuery] = useState("");
  const [questions, setQuestions] = useState<any[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const controller = new AbortController();

    async function fetchQuestions() {
      setLoading(true);
      try {
        if (query.trim()) {
          const results = await client.search.query({ q: query, limit: 20 });
          setQuestions(results.data.results ?? []);
        } else {
          const response = await client.questions.list({ limit: 50 });
          setQuestions(response.data);
        }
      } catch {
        // Ignore aborted requests
      } finally {
        setLoading(false);
      }
    }

    const debounce = setTimeout(fetchQuestions, 300);
    return () => {
      clearTimeout(debounce);
      controller.abort();
    };
  }, [query]);

  return (
    <section className="faq-section">
      <h2>Frequently Asked Questions</h2>

      <input
        type="search"
        placeholder="Search FAQ..."
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        className="faq-search"
        aria-label="Search frequently asked questions"
      />

      {loading ? (
        <div className="faq-loading">Searching...</div>
      ) : questions.length === 0 ? (
        <p className="faq-empty">No results found. Try a different search term.</p>
      ) : (
        <div className="faq-list">
          {questions.map((item) => (
            <FAQItem
              key={item.id || item.slug}
              question={item.question}
              answer={item.answer}
            />
          ))}
        </div>
      )}
    </section>
  );
}

The 300ms debounce prevents excessive API calls while the user types. For more on implementing FAQ search, see our FAQ search implementation guide.

Step 4: Add FAQ Schema Markup (JSON-LD)

Google displays FAQ rich results when your page includes JSON-LD FAQ schema. This can significantly improve click-through rates from search results.

Add a component that generates the schema markup:

// components/FAQSchema.tsx
interface FAQSchemaProps {
  items: Array<{ question: string; answer: string }>;
}

export function FAQSchema({ items }: FAQSchemaProps) {
  const schema = {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    mainEntity: items.map((item) => ({
      "@type": "Question",
      name: item.question,
      acceptedAnswer: {
        "@type": "Answer",
        text: item.answer,
      },
    })),
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

Use it alongside your FAQ component:

import { FAQ } from "./components/FAQ";
import { FAQSchema } from "./components/FAQSchema";

function HelpPage() {
  const faqs = useFAQ("billing");

  return (
    <>
      <FAQSchema items={faqs.questions} />
      <FAQ category="billing" title="Billing FAQ" />
    </>
  );
}

If you use thefaq.app's hosted FAQ pages, JSON-LD schema markup is generated automatically on every page — no code required.

Step 5: Accessibility

A good FAQ component is keyboard-navigable and screen-reader friendly. Update FAQItem with proper ARIA attributes:

// components/FAQItem.tsx (accessible version)
import { useState, useId } from "react";

interface FAQItemProps {
  question: string;
  answer: string;
  defaultOpen?: boolean;
}

export function FAQItem({ question, answer, defaultOpen = false }: FAQItemProps) {
  const [isOpen, setIsOpen] = useState(defaultOpen);
  const id = useId();
  const answerId = `faq-answer-${id}`;

  return (
    <div className="faq-item">
      <h3>
        <button
          className="faq-question"
          onClick={() => setIsOpen(!isOpen)}
          aria-expanded={isOpen}
          aria-controls={answerId}
        >
          <span>{question}</span>
          <span className="faq-icon" aria-hidden="true">
            {isOpen ? "−" : "+"}
          </span>
        </button>
      </h3>
      <div
        id={answerId}
        className="faq-answer"
        role="region"
        aria-labelledby={`faq-question-${id}`}
        hidden={!isOpen}
      >
        <div dangerouslySetInnerHTML={{ __html: answer }} />
      </div>
    </div>
  );
}

Key accessibility features:

  • aria-expanded tells screen readers whether the answer is visible
  • aria-controls links the button to the answer panel
  • role="region" marks the answer as a distinct content region
  • hidden attribute properly hides collapsed answers from assistive technology
  • The question button is wrapped in an <h3> for proper heading hierarchy

Step 6: Styling

Here's minimal CSS that gives you a clean, professional FAQ layout:

/* styles/faq.css */
.faq-section {
  max-width: 720px;
  margin: 0 auto;
  padding: 2rem 1rem;
}

.faq-section h2 {
  font-size: 1.75rem;
  margin-bottom: 1.5rem;
}

.faq-search {
  width: 100%;
  padding: 0.75rem 1rem;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  font-size: 1rem;
  margin-bottom: 1.5rem;
}

.faq-search:focus {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
  border-color: transparent;
}

.faq-item {
  border-bottom: 1px solid #e2e8f0;
}

.faq-question {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  padding: 1rem 0;
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1.05rem;
  font-weight: 500;
  text-align: left;
  color: inherit;
}

.faq-question:hover {
  color: #3b82f6;
}

.faq-icon {
  font-size: 1.25rem;
  flex-shrink: 0;
  margin-left: 1rem;
}

.faq-answer {
  padding: 0 0 1rem;
  color: #4a5568;
  line-height: 1.7;
}

.faq-loading,
.faq-error,
.faq-empty {
  text-align: center;
  padding: 2rem;
  color: #718096;
}

The Fast Path: Skip the Build

Building a FAQ component from scratch gives you full control, but it takes time. If you want a FAQ page running in minutes instead of hours:

Option 1: Embed the widget. Add a single script tag and get a searchable, themed FAQ widget on any page:

<script
  src="https://www.thefaq.app/widget/embed.js"
  data-org="your-org"
  data-theme="light"
  async
></script>

Option 2: Use the SDK. Install @faqapp/core and build exactly the UI you want — with content managed through the dashboard instead of your codebase.

Option 3: Hosted pages. Get a fully SEO-optimized FAQ page at your-org.thefaq.app with zero code. Connect a custom domain like faq.yourcompany.com for a branded experience.

All three options include automatic FAQ schema markup, search, and category filtering — built in.

Summary

ApproachBest ForContent UpdatesSEOSetup Time
Hard-coded JSX3-5 static questionsRequires deployManual schemaMinutes
Custom + APIFull control over UIInstant via APIManual schemaHours
thefaq.app widgetQuick integrationInstant via dashboardAutomaticMinutes
thefaq.app hostedZero-code FAQ pageInstant via dashboardAutomaticMinutes

For most teams, the sweet spot is using the SDK with a custom React component — you get full UI control with API-managed content. As your FAQ grows, add search and categories through the API instead of rebuilding your component.

Ready to stop hard-coding FAQ content? Start free with thefaq.app — API access included on every plan, including the free tier.

Related


TheFAQApp Team

We build the API-first FAQ platform for developer teams. Our mission is to make FAQ management as easy as managing code.

Ready to build your FAQ?

Create searchable, API-powered FAQ pages in minutes. Free to start — no credit card required.

Continue reading

Get developer updates

API changelog, new features, and FAQ best practices. No spam.