Basic Search
Add search functionality by providingsearchQuery and searchMethod props:
import React, { useState } from 'react';
import ReactAppleTree from '@newtonschool/react-apple-tree';
const SearchableTree = () => {
const [treeData, setTreeData] = useState([
{
id: 1,
title: 'Node 1',
children: [{ id: 2, title: 'Node 1.1' }],
},
{
id: 3,
title: 'Node 2',
children: [
{ id: 4, title: 'Node 2.1' },
{ id: 5, title: 'Node 2.2', children: [{ id: 6, title: 'Node 2.2.1' }] },
],
},
{
id: 7,
title: 'Node 3',
},
]);
const [searchQuery, setSearchQuery] = useState('');
return (
<div>
<input
type="text"
placeholder="Search tree..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
style={{
width: '100%',
padding: '8px 12px',
marginBottom: '16px',
fontSize: '14px',
border: '1px solid #ddd',
borderRadius: '4px',
}}
/>
<div style={{ height: 400 }}>
<ReactAppleTree
treeData={treeData}
onChange={setTreeData}
getNodeKey={({ node }) => node.id}
searchQuery={searchQuery}
searchMethod={({ node, searchQuery }) => {
if (node.title) {
return String(node.title)
.toLowerCase()
.includes(searchQuery.toLowerCase());
}
return false;
}}
/>
</div>
</div>
);
};
The
searchMethod function is called for each node. Return true if the node matches the search query.Custom Search Methods
Implement different search strategies:searchMethod={({ node, searchQuery }) => {
if (!searchQuery) return false;
return String(node.title).includes(searchQuery);
}}
Search with React Elements
If your nodes use React elements for titles, extract text for searching:import { getReactElementText } from '@newtonschool/react-apple-tree/utils';
searchMethod={({ node, searchQuery }) => {
if (!searchQuery) return false;
let titleText = '';
if (typeof node.title === 'object') {
// Extract text from React element
titleText = getReactElementText(node.title);
} else {
titleText = String(node.title || '');
}
return titleText.toLowerCase().includes(searchQuery.toLowerCase());
}}
Search Focus Navigation
Navigate through search matches usingsearchFocusOffset:
import React, { useState, useEffect } from 'react';
import ReactAppleTree from '@newtonschool/react-apple-tree';
const SearchWithNavigation = () => {
const [treeData, setTreeData] = useState([/* ... */]);
const [searchQuery, setSearchQuery] = useState('');
const [searchFocusIndex, setSearchFocusIndex] = useState(0);
const [matchCount, setMatchCount] = useState(0);
// Reset focus index when search query changes
useEffect(() => {
setSearchFocusIndex(0);
}, [searchQuery]);
const goToNextMatch = () => {
setSearchFocusIndex((prev) => (prev + 1) % matchCount);
};
const goToPrevMatch = () => {
setSearchFocusIndex((prev) => (prev - 1 + matchCount) % matchCount);
};
return (
<div>
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
<input
type="text"
placeholder="Search..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
style={{ flex: 1, padding: '8px 12px' }}
/>
<button onClick={goToPrevMatch} disabled={matchCount === 0}>
⬆️ Previous
</button>
<button onClick={goToNextMatch} disabled={matchCount === 0}>
⬇️ Next
</button>
<span style={{ alignSelf: 'center', color: '#666' }}>
{matchCount > 0 ? `${searchFocusIndex + 1} / ${matchCount}` : 'No matches'}
</span>
</div>
<div style={{ height: 400 }}>
<ReactAppleTree
treeData={treeData}
onChange={setTreeData}
getNodeKey={({ node }) => node.id}
searchQuery={searchQuery}
searchFocusOffset={searchFocusIndex}
searchMethod={({ node, searchQuery }) => {
return String(node.title)
.toLowerCase()
.includes(searchQuery.toLowerCase());
}}
searchFinishCallback={(matches) => {
setMatchCount(matches.length);
}}
/>
</div>
</div>
);
};
The focused match gets
isSearchFocus: true in generateNodeProps, allowing you to style it differently.Expand Only Matched Nodes
UseonlyExpandSearchedNodes to collapse all non-matching branches:
<ReactAppleTree
treeData={treeData}
onChange={setTreeData}
getNodeKey={({ node }) => node.id}
searchQuery={searchQuery}
searchMethod={({ node, searchQuery }) => {
return String(node.title)
.toLowerCase()
.includes(searchQuery.toLowerCase());
}}
onlyExpandSearchedNodes={true}
/>
Search Finish Callback
UsesearchFinishCallback to get information about search results:
const [searchStats, setSearchStats] = useState({
totalMatches: 0,
matchedPaths: [],
});
<ReactAppleTree
treeData={treeData}
onChange={setTreeData}
getNodeKey={({ node }) => node.id}
searchQuery={searchQuery}
searchMethod={({ node, searchQuery }) => {
return String(node.title)
.toLowerCase()
.includes(searchQuery.toLowerCase());
}}
searchFinishCallback={(matches) => {
setSearchStats({
totalMatches: matches.length,
matchedPaths: matches.map(m => m.path),
});
console.log('Search results:', matches);
// matches is an array of { node, path, treeIndex }
}}
/>
{searchQuery && (
<div style={{ marginTop: 8, color: '#666' }}>
Found {searchStats.totalMatches} match{searchStats.totalMatches !== 1 ? 'es' : ''}
</div>
)}
Highlighting Search Matches
Style search matches usinggenerateNodeProps:
<ReactAppleTree
treeData={treeData}
onChange={setTreeData}
getNodeKey={({ node }) => node.id}
searchQuery={searchQuery}
searchMethod={({ node, searchQuery }) => {
return String(node.title)
.toLowerCase()
.includes(searchQuery.toLowerCase());
}}
generateNodeProps={({ node, isSearchMatch, isSearchFocus }) => ({
style: {
backgroundColor: isSearchFocus
? '#ffd54f' // Bright yellow for focused match
: isSearchMatch
? '#fff9c4' // Light yellow for other matches
: 'transparent',
borderLeft: isSearchFocus ? '3px solid #ffa000' : 'none',
padding: '4px 8px',
borderRadius: '4px',
transition: 'all 0.2s ease',
},
})}
/>
Advanced: Highlight Search Terms
Highlight the actual search term within the title:const highlightText = (text, query) => {
if (!query) return text;
const parts = text.split(new RegExp(`(${query})`, 'gi'));
return (
<span>
{parts.map((part, i) =>
part.toLowerCase() === query.toLowerCase() ? (
<mark key={i} style={{ backgroundColor: '#ffd54f', padding: '0 2px' }}>
{part}
</mark>
) : (
<span key={i}>{part}</span>
)
)}
</span>
);
};
<ReactAppleTree
treeData={treeData}
onChange={setTreeData}
getNodeKey={({ node }) => node.id}
searchQuery={searchQuery}
searchMethod={({ node, searchQuery }) => {
return String(node.title)
.toLowerCase()
.includes(searchQuery.toLowerCase());
}}
generateNodeProps={({ node }) => ({
title: () => highlightText(node.title, searchQuery),
})}
/>
Complete Search Example
Here’s a full example with all search features:import React, { useState, useEffect } from 'react';
import ReactAppleTree from '@newtonschool/react-apple-tree';
const FullSearchExample = () => {
const [treeData, setTreeData] = useState([
{
id: 1,
title: 'Documents',
children: [
{ id: 2, title: 'Report.pdf' },
{ id: 3, title: 'Invoice.pdf' },
],
},
{
id: 4,
title: 'Images',
children: [
{ id: 5, title: 'Photo1.jpg' },
{ id: 6, title: 'Photo2.jpg' },
],
},
]);
const [searchQuery, setSearchQuery] = useState('');
const [searchFocusIndex, setSearchFocusIndex] = useState(0);
const [matchCount, setMatchCount] = useState(0);
const [collapseNonMatches, setCollapseNonMatches] = useState(false);
useEffect(() => {
setSearchFocusIndex(0);
}, [searchQuery]);
return (
<div>
<div style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<input
type="text"
placeholder="Search tree..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
style={{ flex: 1, padding: '8px 12px' }}
/>
<button
onClick={() => setSearchFocusIndex((prev) => Math.max(0, prev - 1))}
disabled={matchCount === 0}
>
⬆️
</button>
<button
onClick={() => setSearchFocusIndex((prev) => Math.min(matchCount - 1, prev + 1))}
disabled={matchCount === 0}
>
⬇️
</button>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: '14px' }}>
<label>
<input
type="checkbox"
checked={collapseNonMatches}
onChange={(e) => setCollapseNonMatches(e.target.checked)}
/>
{' '}Only show matches
</label>
<span style={{ marginLeft: 'auto', color: '#666' }}>
{searchQuery && (
matchCount > 0
? `${searchFocusIndex + 1} / ${matchCount} matches`
: 'No matches'
)}
</span>
</div>
</div>
<div style={{ height: 400 }}>
<ReactAppleTree
treeData={treeData}
onChange={setTreeData}
getNodeKey={({ node }) => node.id}
searchQuery={searchQuery}
searchFocusOffset={searchFocusIndex}
onlyExpandSearchedNodes={collapseNonMatches}
searchMethod={({ node, searchQuery }) => {
if (!searchQuery) return false;
return String(node.title)
.toLowerCase()
.includes(searchQuery.toLowerCase());
}}
searchFinishCallback={(matches) => {
setMatchCount(matches.length);
}}
generateNodeProps={({ node, isSearchMatch, isSearchFocus }) => ({
style: {
backgroundColor: isSearchFocus
? '#ffd54f'
: isSearchMatch
? '#fff9c4'
: 'transparent',
borderLeft: isSearchFocus ? '3px solid #ffa000' : 'none',
padding: '4px 8px',
borderRadius: '4px',
},
})}
/>
</div>
</div>
);
};
Search Performance Tips
For large trees with thousands of nodes:
- Debounce search input - Wait for user to stop typing
- Cache search results - Don’t re-search unchanged queries
- Use efficient string matching - Consider libraries like
fuse.jsfor fuzzy search - Enable virtualization - Keep
isVirtualized={true}(default)
import { useMemo } from 'react';
import debounce from 'lodash/debounce';
const [searchInput, setSearchInput] = useState('');
const [searchQuery, setSearchQuery] = useState('');
// Debounce search to avoid excessive re-renders
const debouncedSetSearch = useMemo(
() => debounce((value) => setSearchQuery(value), 300),
[]
);
const handleSearchChange = (e) => {
const value = e.target.value;
setSearchInput(value);
debouncedSetSearch(value);
};
Next Steps
- Advanced Features - Multiple trees, RTL, and performance
- API Reference - Complete search props reference
- Helper Functions - Utility functions for tree manipulation