SR
Click to open github profile
Get Started

Quick Start

Learn how to install and configure the @query-api/react package.


Install

npm install @query-api/react

Requirements

Before you start be sure to have the following prerequisites to follow this guide:

  • Craft CMS + Query API successfully installed and running
  • A Craft CMS section with at least one entry type (I used sections called home and news with entry types home)
  • A React application set up with TypeScript (e.g., npm create vite@latest react-demo -- --template react-ts)
Note

If you prefer to dive straight into code or need a reference, check out the demo project.

Connect to Craft CMS

To use the Query API in your React application, you need to initialize the client with your Craft CMS backend details and map your React components to Craft Entries. You can do this in your main entry file, typically main.tsx or index.tsx.

Here's an example of how to set it up:

main.tsx
import { StrictMode } from 'react'
import { BrowserRouter } from 'react-router'
import * as ReactDOM from 'react-dom/client'
import App from './app/app'
// craftInit is the main function to initialize the Query API client.
// CraftNotImplemented is a placeholder for components that are not implemented yet.
import { craftInit, CraftNotImplemented } from '@query-api/react'

// Import your React components that will be used in your Craft CMS Entries.
import Home from './app/views/Home'
import News from './app/views/News'
import Headline from './app/components/Headline'

craftInit({
  baseUrl: 'https://your-craft-backend.ddev.site',
  authToken: 'Bearer yourBearerToken',
  contentMapping: {
    pages: {
      home: Home, // Maps section home entry with entry type home to the Home component.
      'news:home': News, // Maps section news entry with entry type home to the News component.
    },
    components: {
      headline: Headline, // Entry type headline will be rendered with the Headline component.
      imageText: CraftNotImplemented,
    },
  },
})
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)

root.render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>,
)
Note

To find more about the configuration options, check out the Configuration Docs.

A few words about contentMapping:

This tells the Query API which React components to use for each Craft CMS section or entry type.

In pages, map sections and entry types to React pages using sectionHandle:entryTypeHandle (e.g., news:home). If the entry type matches the section handle or is default, just use the section handle (e.g., home).

In components, map entry types to React components—useful for matrix blocks.


Display Page

To display the content from Craft CMS, you first need to fetch the data and then you can use the CraftPage component. This component will automatically render the pages based on the contentMapping configuration.

Add Fetch Composable

To fetch data from Craft CMS, create a useCraftFetch.ts file in your composables directory. This will be a custom hook that uses the Fetch API to retrieve data from your Craft CMS backend.

Tip

If you are using Tanstack Query, you can use the useQuery hook instead.

useCraftFetch.ts
import { useEffect, useState } from 'react'

interface UseCraftFetchResult<T> {
  data: T | null
  loading: boolean
  error: string | null
}

export function useCraftFetch<T = object>(url: string | null, authToken?: string): UseCraftFetchResult<T> {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    if (!url || !authToken) {
      throw new Error("Please provide a valid url and auth token")
    }

    setLoading(true)
    setError(null)

    fetch(url, {
      headers: { 
        Authorization: authToken 
      }
    }).then(async (res) => {
        if (!res.ok) throw new Error(await res.text())
        return res.json()
      })
      .then(setData)
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false))
  }, [url, authToken])

  return { data, loading, error }
}
export default useCraftFetch

Configure Routing

Next, create a CraftRouter.tsx file in your app directory. This file will handle the routing and rendering of the pages based on the data fetched from Craft CMS.

CraftRouter.tsx
import { useParams } from 'react-router'
import { CraftPage, getCraftInstance, createCraftUrl } from '@query-api/react'
import { useCraftFetch } from './composables/useCraftFetch'
import type { CraftPageBase } from '../types'

export default function CraftRouter() {
  const { '*': params } = useParams()
  const uri = params !== '' ? params : '__home__'

  const { authToken } = getCraftInstance()
  const apiUrl = createCraftUrl('entries').uri(uri).buildUrl('one')

  const { data, loading, error } = useCraftFetch<CraftPageBase>(apiUrl, authToken)

  if (error) {
    console.error(error)
  }
  return (
    <div>
      {!loading && data && <CraftPage content={data} />}
    </div>
  )
}

Now you can use the CraftRouter component in your main app.tsx file to handle the routing and rendering of the pages.

For me it looks like this:

app.tsx
import { Route, Routes, Link } from 'react-router'
import CraftRouter from './CraftRouter'

export function App() {
  return (
    <div>
      <nav role="navigation">
        <Link to="/">Home</Link>
        <Link to="/news-article-1">News Article 1</Link>
      </nav>
      <Routes>
        <Route path="*" element={<CraftRouter />} /> // Catch-all route to handle all paths
      </Routes>
    </div>
  )
}

export default App

You can now navigate to different pages in your Craft CMS site, and the CraftRouter should fetch the data and render the appropriate page using the CraftPage component. The data of the fetch will be passed as prop to the mapped React component.

To display the data, you simply use this code:

News.tsx
import type { CraftPageHome } from '../../types'

export default function News(props: CraftPageHome) {
  return (
    <pre>
      {JSON.stringify(props, null, 2)}
    </pre>
  )
}
Note

That CraftPageHome type is a generated type that represents the data structure of the home entry type in your Craft CMS. You can read more about how to generate these types in the Query API Commands Docs.

Display Components

Let's say you have a matrix block with the handle contentBuilder in your home entry type. You can render these blocks using the CraftArea component. This component will dynamically render React components based on the content provided from Craft CMS.

Home.tsx
import type { CraftPageHome } from '../../types'

export default function Home(props: CraftPageHome) {
  return (
    <div>
      <h1>{props.title}</h1>
      <CraftArea content={props.contentBuilder} />
    </div>
  )
}

Standalone Queries

If you want to fetch data from Craft CMS without using the CraftPage component, you can use the useCraftFetch hook directly in your components. Let's say you want to show a list of news articles on your homepage. You can do this by fetching the data directly from the Craft CMS API.

Home.tsx
import type { CraftPageHome } from '../../types'
import { CraftArea, getCraftInstance, createCraftUrl } from '@query-api/react'
import { useCraftFetch } from '../composables/useCraftFetch'

export default function Home(props: CraftPageHome) {
  const {authToken} = getCraftInstance()
  const url = createCraftUrl('entries').section('news').limit(3).fields(['title']).buildUrl('all')
  const {data: news, loading, error} = useCraftFetch<CraftPageHome[]>(url, authToken) 

  if (error) {
    console.error(error)
  }
  return (
    <div>
      <h1>{props.title}</h1>
      <CraftArea content={props.contentBuilder} />

      <h2>Related News</h2>
      {!loading && news && (
        <ul>
          {news.map((item, idx) => (
            <li key={idx}>
              <a href={item.metadata.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}

Anything missing?

If you have any questions, run into issues, or have ideas for improvements, your feedback is very welcome! Please don't hesitate to open an issue on GitHub. Whether it's a bug report, feature request, or general suggestion, your input helps make this project better for everyone.


Copyright © 2025 Samuel Reichör