Skip to main content

React SDK

The @viglet/turing-react-sdk provides headless hooks and unstyled UI components for building custom search experiences on top of the Viglet Turing ES Semantic Navigation API.

  • Headless architecture: zero built-in styles, full render control via render props
  • Two modes: manual (programmatic) or URL-synced (URL as source of truth)
  • TypeScript-first: full type safety for all hooks, components, and API responses
  • React 18 & 19 compatible

Storybook: Interactive demos and live examples at turing.viglet.org/react-sdk


Installation

npm install @viglet/turing-react-sdk

Peer dependencies: react (^18 or ^19) and axios (^1.0).


Quick Start

import { TuringProvider, useTuringSearch } from "@viglet/turing-react-sdk";

function App() {
return (
<TuringProvider config={{ site: "my-site", locale: "en_US" }}>
<SearchPage />
</TuringProvider>
);
}

function SearchPage() {
const { documents, searchQuery, status } = useTuringSearch();

return (
<div>
<input
placeholder="Search..."
onKeyDown={(e) => {
if (e.key === "Enter") searchQuery(e.currentTarget.value);
}}
/>
{status === "loading" && <p>Loading...</p>}
{documents.map((doc) => (
<div key={doc.url}>
<h3>{doc.title}</h3>
<p>{doc.description}</p>
</div>
))}
</div>
);
}

TuringProvider

The root context provider that makes Turing config and search state available to all hooks.

import { TuringProvider } from "@viglet/turing-react-sdk";

Props

PropTypeRequiredDescription
configTuringConfigYesSite configuration
urlSync{ searchParams, setSearchParams }NoEnable URL synchronization
childrenReactNodeYesChild components

TuringConfig

{
site: string; // Turing site name (required)
locale?: string; // Default locale (e.g., "en_US")
sort?: string; // Default sort (e.g., "relevance")
}

URL Sync Mode

When urlSync is provided, the URL becomes the single source of truth. Changes to the URL trigger automatic searches, and searches update the URL automatically.

import { useSearchParams } from "react-router-dom";

function App() {
const [searchParams, setSearchParams] = useSearchParams();
return (
<TuringProvider
config={{ site: "my-site", locale: "pt_BR" }}
urlSync={{ searchParams, setSearchParams }}
>
<SearchPage />
</TuringProvider>
);
}

Hooks

useTuringSearch

Core search hook for manual (non-URL) mode. Manages the full lifecycle: query, results, pagination, facets, locale, and sort.

const {
status, // "idle" | "loading" | "success" | "error"
data, // Raw TurSearchResponse
chat, // AI chat response (null for wildcard queries)
documents, // Resolved documents with mapped default fields
groups, // Resolved groups (when group param used)
error, // Error message
params, // Current search params
structured, // Link-free response (POST-native; authoritative)
request, // The request that will be sent next
search, // Execute with given params
searchRequest, // Execute from an explicit structured request
searchQuery, // New text query (resets page to 1)
toggleFacet, // Toggle a structured facet item — or a result's own facet chip
clearFacet, // Clear one facet
clearAllFilters,
applySpellCheck, // Accept (or refuse) "did you mean"
changeLocale, // Change locale
changeSort, // Change sort
goToPage, // Go to specific page
} = useTuringSearch(initialParams?);

The hook is POST-native by default: it calls the POST-native search endpoint and asks for the link-free response, so structured is authoritative and every action above is a change to request rather than a Turing href to parse. That is what makes a filter value containing &, =, +, #, % or a space safe.

data (the legacy link-bearing body) is null in this mode. Pass { postNative: false } to go back to the GET contract and data, and render its hrefs as real anchors. The deprecated navigate(href) still works on that path but logs a one-time console notice and is scheduled for removal in the next major; the same applies to onNavigate on TuringPagination.

useTuringAutoComplete

Autocomplete hook that calls the Turing /ac endpoint with built-in debouncing.

const {
suggestions, // string[]: completion suggestions
isLoading, // Fetching state
fetch, // Trigger autocomplete (min 2 chars)
clear, // Clear suggestions
} = useTuringAutoComplete(debounceMs?: number); // default: 300

useTuringFacets

Provides facet data with toggle/clear actions. Requires urlSync mode.

const {
facetGroups, // Groups with toggle/clear actions
hasSelected, // Any facet selected?
clearAll, // Remove all selections
raw, // Raw API facet data
} = useTuringFacets();

Each facetGroup contains:

{
name: string; // Group identifier
label: string; // Localized label
multivalued: boolean;
hasSelected: boolean;
clear: () => void; // Clear group selections
facets: [{
count: number;
label: string;
selected: boolean;
toggle: () => void; // Toggle this facet
link?: string; // Legacy mode only — see below
}];
}

link is the server-rendered href for a facet value and exists only in legacy mode (postNative: false). The default POST-native mode returns a link-free response, so it is undefined there — use toggle(), and if you need a crawlable anchor, handle the absence explicitly rather than rendering <a href={facet.link}> (which would link to the current page).

useTuringPagination

Provides pagination data with navigation helpers. Requires urlSync mode.

const {
pages, // All pages with action helpers
currentPage, // Current page number
pageCount, // Total pages
totalCount, // Total results
hasPages, // Multiple pages?
} = useTuringPagination();

Each page contains:

{
page: number;
label: string; // "1", "2", "Next", etc.
type: string; // PAGE | CURRENT | FIRST | PREVIOUS | NEXT | LAST | ELLIPSIS
isCurrent: boolean;
isEllipsis: boolean;
select: () => void; // Navigate to this page
}

useTuringDocument

Typed, normalized access to document custom fields, eliminating Array.isArray() checks.

const {
getString, // (field, fallback?) => string
getArray, // (field) => string[]
getNumber, // (field, fallback?) => number
getBoolean, // (field) => boolean
getDate, // (field) => Date | null
getRaw, // <T>(field) => T | undefined
fields, // Direct field access
} = useTuringDocument(document);

useTuringTabs

Manages tab-based search with filter switching and grouping. Keeps URLs clean using implicit params.

const {
activeIndex, // Active tab index
activeTab, // Active tab definition
tabs, // All tabs with select() action
isGrouped, // Active tab uses grouping?
documents, // Docs for active tab
groups, // Groups (if grouped)
} = useTuringTabs({
attribute: "templateName",
items: [
{ label: "All" },
{ label: "Courses", filter: "templateName:cursos", group: "category" },
{ label: "News", filter: "templateName:noticias", rows: 5 },
],
});

useTuringSortOptions

Fetches available sort options (built-in + custom from Turing admin console).

const {
sortOptions, // { value: string, label: string }[]
isLoading,
error,
refresh, // Re-fetch options
} = useTuringSortOptions();

useTuringSearchHistory

Persists search terms in IndexedDB, scoped per site + domain.

const {
history, // Recent terms (newest first, deduplicated)
save, // Save a term
remove, // Remove a term
clear, // Clear all for current site
} = useTuringSearchHistory(maxItems?: number); // default: 50

useTuringUrlSearch

Returns the shared search store from TuringProvider when urlSync is configured.

const store = useTuringUrlSearch();
// Same shape as TuringProvider's internal store:
// status, structured, data, documents, groups, params, inputValue,
// submitSearch, toggleFacet, goToPage, navigate, setLocale, setSort, showAll, etc.

structured holds the link-free response — the default. data holds the legacy link-bearing one and is null unless you set urlSync.postNative: false. Prefer the derived hooks above (and useSearchView below) over reading either directly.

useSearchView

Projects whichever response arrived onto one view model, so a component never learns which shape it got. It covers the parts the other hooks do not — the site icon, the locales, the applied-filter row and the spell-check offer — and resolves each action with its state: removing an applied filter subtracts a filter query in the default mode and follows an href on the legacy transport, through the same remove() call.

const view = useSearchView();
if (!view.ready) return <Spinner />; // the render gate, either shape

return (
<>
{view.siteIcon ? <Icon icon={view.siteIcon} /> : <YourLogo />}
{view.appliedFilters.map((filter) => (
<button key={filter.key} onClick={filter.remove}>{filter.label}</button>
))}
{view.spellCheck?.correctedText && (
<button onClick={view.spellCheck.applyCorrection}>
Did you mean {view.spellCheck.correctedTextValue}?
</button>
)}
<FacetRail multiSelect={view.multiSelectFacetItems} />
</>
);
FieldDescription
source"structured" | "legacy" | "none" — which shape backed the view
readytrue once a response of either shape arrived
siteIconThe site's Iconify name, or null — fall back to your own logo
locales[{ locale }] the site can be searched in
queryContextCounts, paging and default field names
appliedFilters[{ key, label, remove() }] — the "applied filters" row
spellCheck{ correctedTextValue, applyCorrection(), applyOriginal() } or null
multiSelectFacetItemstrue when the site's facets multi-select (draw checkboxes)

The projection itself is toSearchView(store) in @viglet/turing-sdk — the same function, so a plain-JS host and a React one resolve a response identically. This hook is the React wrapper.


UI Components

All components are unstyled (headless). You control the look via render props and CSS.

TuringSearchBar

Simple search bar with customizable input/button via render props.

<TuringSearchBar
onSearch={(query) => console.log(query)}
placeholder="Search..."
defaultQuery=""
renderInput={(props) => <input {...props} className="my-input" />}
renderButton={({ onClick }) => <button onClick={onClick}>Go</button>}
/>
PropTypeDescription
onSearch(query: string) => voidCallback on Enter or button click
defaultQuerystringPre-fill value
placeholderstringInput placeholder
renderInput(props) => ReactNodeCustom input renderer
renderButton(props) => ReactNodeCustom button renderer

TuringSearchField

Advanced compound component with autocomplete, search history, and dropdown.

<TuringSearchField turing={urlSearchStore} debounce={300} maxHistory={8}>
<TuringSearchField.Input placeholder="Search..." className="my-input" />
<TuringSearchField.Button className="my-btn">Search</TuringSearchField.Button>
<TuringSearchField.Dropdown
className="my-dropdown"
historyLabel="Recent searches"
clearAllLabel="Clear all"
renderSuggestion={(term, onClick) => (
<div onClick={onClick}>{term}</div>
)}
renderHistory={(term, onClick, onRemove) => (
<div>
<span onClick={onClick}>{term}</span>
<button onClick={onRemove}>x</button>
</div>
)}
/>
</TuringSearchField>

TuringResultList

Renders search results using the slot/render-prop pattern.

<TuringResultList
documents={documents}
isLoading={status === "loading"}
itemComponent={({ document, index }) => (
<div key={document.url}>
<h3>{document.title}</h3>
<p>{document.description}</p>
</div>
)}
emptyComponent={() => <p>No results found.</p>}
loadingComponent={() => <p>Loading...</p>}
/>
PropTypeDescription
documentsResolvedDocument[]Documents to render
itemComponent(props) => ReactNodeItem renderer (required)
emptyComponent() => ReactNodeNo-results state
loadingComponent() => ReactNodeLoading state
isLoadingbooleanShow loading component
onResultClick(document, index) => voidFires on a row click — wire it to trackResultClick
onToggleFacet(target) => voidToggles a result's facet chip; forwarded to itemComponent
onNavigate(href) => voidLegacy: follows a chip's href; forwarded the same way

TuringDocumentChips

Renders a result's facet chips (document.raw.metadata) as actions. A chip arrives in one of two shapes — a legacy href or a link-free filterQuery — and this narrows that for you: a filter query becomes an aria-pressed button, an href becomes an anchor that still carries the URL (for middle-click and crawlers) but calls your handler, and a chip you gave no handler for renders as plain text rather than as a control that does nothing.

<TuringResultList
documents={documents}
onToggleFacet={toggleFacet}
itemComponent={({ document, raw, onToggleFacet }) => (
<article>
<h3>{document.title}</h3>
<TuringDocumentChips
chips={raw.metadata}
onToggleFacet={onToggleFacet}
chipClassName="chip"
selectedChipClassName="chip--on"
/>
</article>
)}
/>
PropTypeDescription
chipsTuringDocumentChip[]The document's metadata array, either shape
onToggleFacet(target) => voidOmit it and every chip renders inert
onNavigate(href) => voidLegacy chips only
itemComponent(props) => ReactNodeOwn markup; receives chip, isActionable, isSelected, onClick
chipClassName / selectedChipClassNamestringSkin the default renderer

Also exported from @viglet/turing-react-ui for hosts that consume the headless package directly.

TuringPagination

Renders pagination items with navigation actions.

<TuringPagination
items={structured?.pagination ?? []}
onSelectPage={(page) => goToPage(page)}
itemComponent={({ item, isCurrent, isEllipsis, onClick, label }) => (
<button
onClick={onClick}
disabled={isCurrent}
style={{ fontWeight: isCurrent ? "bold" : "normal" }}
>
{isEllipsis ? "..." : label}
</button>
)}
/>

Data Types

ResolvedDocument

Documents returned by hooks have default fields already mapped:

{
url: string; // Document URL
title: string; // Document title
description: string; // Document description
date: string; // Document date
image: string; // Image URL
text: string; // Full text
raw: TurDocument; // Full document with all custom fields
}

SearchParams

{
q: string; // Query string
p?: string; // Page number
_setlocale?: string; // Locale
sort?: string; // Sort field
fq?: string[]; // Facet queries (filters)
group?: string; // Group field name
rows?: string; // Results per page
}

Full Example: Search Page with Facets

import {
TuringProvider,
useTuringUrlSearch,
useTuringFacets,
useTuringPagination,
TuringResultList,
TuringSearchBar,
TuringPagination,
} from "@viglet/turing-react-sdk";
import { useSearchParams } from "react-router-dom";

function App() {
const [searchParams, setSearchParams] = useSearchParams();
return (
<TuringProvider
config={{ site: "my-site", locale: "en_US" }}
urlSync={{ searchParams, setSearchParams }}
>
<SearchPage />
</TuringProvider>
);
}

function SearchPage() {
const store = useTuringUrlSearch();
const { facetGroups, clearAll } = useTuringFacets();
const { pages } = useTuringPagination();

return (
<div>
<TuringSearchBar onSearch={(q) => store.searchQuery(q)} />

<aside>
{facetGroups.map((group) => (
<div key={group.name}>
<h4>{group.label}</h4>
{group.facets.map((facet) => (
<label key={facet.label}>
<input
type="checkbox"
checked={facet.selected}
onChange={facet.toggle}
/>
{facet.label} ({facet.count})
</label>
))}
</div>
))}
</aside>

<main>
<TuringResultList
documents={store.documents}
isLoading={store.status === "loading"}
itemComponent={({ document }) => (
<article>
<h3><a href={document.url}>{document.title}</a></h3>
<p>{document.description}</p>
</article>
)}
emptyComponent={() => <p>No results found.</p>}
/>

<TuringPagination
items={store.structured?.pagination ?? []}
onSelectPage={store.goToPage}
/>
</main>
</div>
);
}


PageDescription
Developer GuideDev environment, stack, contributing
Client ToolsLive tool activity, client tools, generative UI
Reference showcaseAtlas Store: a full app built on this SDK
ChatThe chat surface the hooks drive
REST API ReferenceThe endpoints behind the hooks