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
| Prop | Type | Required | Description |
|---|---|---|---|
config | TuringConfig | Yes | Site configuration |
urlSync | { searchParams, setSearchParams } | No | Enable URL synchronization |
children | ReactNode | Yes | Child 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} />
</>
);
| Field | Description |
|---|---|
source | "structured" | "legacy" | "none" — which shape backed the view |
ready | true once a response of either shape arrived |
siteIcon | The site's Iconify name, or null — fall back to your own logo |
locales | [{ locale }] the site can be searched in |
queryContext | Counts, paging and default field names |
appliedFilters | [{ key, label, remove() }] — the "applied filters" row |
spellCheck | { correctedTextValue, applyCorrection(), applyOriginal() } or null |
multiSelectFacetItems | true 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>}
/>
| Prop | Type | Description |
|---|---|---|
onSearch | (query: string) => void | Callback on Enter or button click |
defaultQuery | string | Pre-fill value |
placeholder | string | Input placeholder |
renderInput | (props) => ReactNode | Custom input renderer |
renderButton | (props) => ReactNode | Custom 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>}
/>
| Prop | Type | Description |
|---|---|---|
documents | ResolvedDocument[] | Documents to render |
itemComponent | (props) => ReactNode | Item renderer (required) |
emptyComponent | () => ReactNode | No-results state |
loadingComponent | () => ReactNode | Loading state |
isLoading | boolean | Show loading component |
onResultClick | (document, index) => void | Fires on a row click — wire it to trackResultClick |
onToggleFacet | (target) => void | Toggles a result's facet chip; forwarded to itemComponent |
onNavigate | (href) => void | Legacy: 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>
)}
/>
| Prop | Type | Description |
|---|---|---|
chips | TuringDocumentChip[] | The document's metadata array, either shape |
onToggleFacet | (target) => void | Omit it and every chip renders inert |
onNavigate | (href) => void | Legacy chips only |
itemComponent | (props) => ReactNode | Own markup; receives chip, isActionable, isSelected, onClick |
chipClassName / selectedChipClassName | string | Skin 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>
);
}
Links
Related Pages
| Page | Description |
|---|---|
| Developer Guide | Dev environment, stack, contributing |
| Client Tools | Live tool activity, client tools, generative UI |
| Reference showcase | Atlas Store: a full app built on this SDK |
| Chat | The chat surface the hooks drive |
| REST API Reference | The endpoints behind the hooks |