Creating a custom renderer
The autocomplete-js
package includes everything you need to render a search experience and bind it to your framework.
If you want to build a custom UI that differs from the autocomplete-js
output, for example in React or another frontend framework, the autocomplete-core
package provides all the primitives to build it.
This guide shows how to use all the Autocomplete features for desktop and mobile, with React. Find the final result on CodeSandbox.
You might not need a custom renderer.
Building a custom renderer is an advanced pattern that leverages the autocomplete-core
package to fully control the rendered experience.
You shouldn’t need to use it unless you’ve reached limitations with autocomplete-js
and its templating capabilities.
Import the package
Begin by importing createAutocomplete
from the core package and getAlgoliaResults
from the Algolia preset.
The preset—autocomplete-preset-algolia
—is a utility function to retrieve items from an Algolia index.
1
2
3
4
5
import algoliasearch from 'algoliasearch/lite';
import { createAutocomplete } from '@algolia/autocomplete-core';
import { getAlgoliaResults } from '@algolia/autocomplete-preset-algolia';
// ...
Initialize Autocomplete
The Autocomplete entry point is the createAutocomplete
function, which returns the methods to create the search experience.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const searchClient = algoliasearch(
'latency',
'6be0576ff61c053d5f9a3225e2a90f76'
);
function Autocomplete() {
// (1) Create a React state.
const [autocompleteState, setAutocompleteState] = React.useState({});
const autocomplete = React.useMemo(
() =>
createAutocomplete({
onStateChange({ state }) {
// (2) Synchronize the Autocomplete state with the React state.
setAutocompleteState(state);
},
getSources() {
return [
// (3) Use an Algolia index source.
{
sourceId: 'products',
getItemInputValue({ item }) {
return item.query;
},
getItems({ query }) {
return getAlgoliaResults({
searchClient,
queries: [
{
indexName: 'instant_search',
query,
params: {
hitsPerPage: 4,
highlightPreTag: '<mark>',
highlightPostTag: '</mark>',
},
},
],
});
},
getItemUrl({ item }) {
return item.url;
},
},
];
},
}),
[]
);
// ...
}
- Use a React state for the Autocomplete component to re-render when the Autocomplete state changes.
- Listen to all Autocomplete state changes to synchronize them with the React state.
- This example uses an Algolia index as a source.
This setup gives you access to all the methods you may want to use in the autocomplete
variable in your React components.
Next, you start building the UI.
Use prop getters
Prop getters are methods that return props to use in your components. These props contain accessibility features, event handlers, and so on. They help create a complete experience without exposing their underlying technical elements.
The following snippet shows how to use the getRootProps()
, getInputProps()
, getPanelProps()
, getListProps()
, and getItemProps()
prop getters in the appropriate elements.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function Autocomplete() {
// ...
return (
<div className="aa-Autocomplete" {...autocomplete.getRootProps({})}>
<input className="aa-Input" {...autocomplete.getInputProps({})} />
<div className="aa-Panel" {...autocomplete.getPanelProps({})}>
{autocompleteState.isOpen &&
autocompleteState.collections.map((collection, index) => {
const { source, items } = collection;
return (
<div key={`source-${index}`} className="aa-Source">
{items.length > 0 && (
<ul className="aa-List" {...autocomplete.getListProps()}>
{items.map((item) => (
<li
key={item.objectID}
className="aa-Item"
{...autocomplete.getItemProps({
item,
source,
})}
>
{item.name}
</li>
))}
</ul>
)}
</div>
);
})}
</div>
</div>
);
}
The preceding code demonstrates that you don’t need to worry about keyboard events, or tracking which item is active. Autocomplete handles this under the hood with its prop getters.
At this point, you should already have a usable search box:
Improve input accessibility
To improve the input
control, wrap it in a form
and apply the form props given by Autocomplete:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Autocomplete() {
// ...
const inputRef = React.useRef(null);
return (
<div className="aa-Autocomplete" {...autocomplete.getRootProps({})}>
<form
className="aa-Form"
{...autocomplete.getFormProps({ inputElement: inputRef.current })}
>
<input ref={inputRef} {...autocomplete.getInputProps({})} />
</form>
{/* ... */}
</div>
);
}
The getFormProps
prop getter handles submit and reset events.
It also respectively blurs and focuses the input when these events happen.
Pass the inputElement
when calling getFormProps
to use these features.
Add a label that represents the input and use the getLabelProps
prop getter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Autocomplete() {
// ...
const inputRef = React.useRef(null);
return (
<div className="aa-Autocomplete" {...autocomplete.getRootProps({})}>
<form
className="aa-Form"
{...autocomplete.getFormProps({ inputElement: inputRef.current })}
>
<div className="aa-InputWrapperPrefix">
<label className="aa-Label" {...autocomplete.getLabelProps({})}>
Search
</label>
</div>
<div className="aa-InputWrapper">
<input
className="aa-Input"
ref={inputRef}
{...autocomplete.getInputProps({})}
/>
</div>
</form>
{/* ... */}
</div>
);
}
Another good practice for search inputs is to display a reset button. Display it if there’s a query.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function Autocomplete() {
// ...
return (
<div className="aa-Autocomplete" {...autocomplete.getRootProps({})}>
<form
className="aa-Form"
{...autocomplete.getFormProps({ inputElement: inputRef.current })}
>
<div className="aa-InputWrapperPrefix">
<label className="aa-Label" {...autocomplete.getLabelProps({})}>
Search
</label>
</div>
<div className="aa-InputWrapper">
<input
className="aa-Input"
ref={inputRef}
{...autocomplete.getInputProps({})}
/>
</div>
<div className="aa-InputWrapperSuffix">
<button className="aa-ClearButton"
x
</button>
</div>
</form>
{/* ... */}
</div>
);
}
Check network status
Displaying UI hints when the network is unstable helps users understand why results aren’t updating in real time.
Examine the status
to determine this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Autocomplete() {
// ...
return (
<div className="aa-Autocomplete" {...autocomplete.getRootProps({})}>
{/* ... */}
{autocompleteState.isOpen && (
<div
className={[
'aa-Panel',
autocompleteState.status === 'stalled' && 'aa-Panel--stalled',
]
.filter(Boolean)
.join(' ')}
{...autocomplete.getPanelProps({})}
>
{/* ... */}
</div>
)}
{/* ... */}
</div>
);
}
You could, for example, create a .aa-Panel--stalled
CSS class that lowers items’ opacity to show that search is temporarily stuck or unavailable.
For more information, see Controlling behavior with state.
Mirror a native mobile experience
Native platforms offer better primitives for mobile search experiences. Autocomplete helps provide these capabilities so that the web mobile experience is closer to the native mobile experience.
A common feature in mobile native experiences is to close the virtual keyboard when users start scrolling. This makes the results more discoverable without users having to manually close the keyboard. The panel closes when users tap outside it.
The getEnvironmentProps
method returns event handlers that let you create this experience:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
function Autocomplete() {
// ...
const inputRef = React.useRef(null);
const formRef = React.useRef(null);
const panelRef = React.useRef(null);
const { getEnvironmentProps } = autocomplete;
React.useEffect(() => {
if (!(formRef.current && panelRef.current && inputRef.current)) {
return;
}
const { onTouchStart, onTouchMove, onMouseDown } = getEnvironmentProps({
formElement: formRef.current,
panelElement: panelRef.current,
inputElement: inputRef.current,
});
window.addEventListener('touchstart', onTouchStart);
window.addEventListener('touchmove', onTouchMove);
window.addEventListener('mousedown', onMouseDown);
return () => {
window.removeEventListener('touchstart', onTouchStart);
window.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('mousedown', onMouseDown);
};
}, [getEnvironmentProps, autocompleteState.isOpen]);
return (
<div className="aa-Autocomplete" {...autocomplete.getRootProps({})}>
<form
ref={formRef}
className="aa-Form"
{...autocomplete.getFormProps({ inputElement: inputRef.current })}
>
{/* ... */}
</form>
{autocompleteState.isOpen && (
<div
ref={panelRef}
className={[
'aa-Panel',
autocompleteState.status === 'stalled' && 'aa-Panel--stalled',
]
.filter(Boolean)
.join(' ')}
{...autocomplete.getPanelProps({})}
>
{/* ... */}
</div>
)}
</div>
);
}
This way, users feel a bit closer to what they’re used to on mobile apps.
Help and discussion
You now have enough knowledge to build your own experience based on Autocomplete. If you find that some topics weren’t covered, feel free to open an issue.