This is perhaps a very silly question.
I am receiving data from an endpoint in the following format. I want to assign the prediction a serial number taking into account the predictions in the previous category.
const items = [{
"category": "Office",
"predictions": [
{
"id": "2599",
"value": "Printer",
"type": "OFF"
},
{
"id": "2853",
"value": "Camera",
"type": "OFF"
},
{
"id": "2202",
"value": "Keyboard",
"type": "OFF"
}
]
},
{
"category": "Home",
"predictions": [
{
"id": "2899",
"value": "Television",
"type": "ELEC"
},
{
"id": "2853",
"value": "Microwave",
"type": "ELEC"
},
{
"id": "2732",
"value": "Washing Machine",
"type": "ELEC"
}
]
}
];
In React, I am doing the following to render the predictions to the customer based on their query:
const [highlighted, setHighlighted] = useState(-1);
// other code
const showPredictions = (predictions, categoryIndex) => {
return (
<ul role="listbox">
{predictions.map((prediction, index) => {
const serialNum = index;
const isHighlighted = highlighted === serialNum;
return (
<li
onMouseEnter={() => handleSuggestionMouseEnter(serialNum)}
className={isHighlighted ? 'highlight' : ''}
>
{prediction.value}
</li>
);
})}
</ul>
);
};
const showPredictionsByCategory = (items) => {
return items.map((category, catIndex) => {
const categoryTitle = renderTitleText(category);
const itemList = showPredictions(category.predictions, catIndex);
return (
<div>
{categoryTitle}
{itemList}
</div>
);
});
};
I want to increment and decrement this serial number so that I can highlight the item on keyup and down. Hence, I want the predictions to be numbered 0, 1, 2, 3,...n ( where n = number of predictions minus 1)
So in the above example, the Office
predictions would have serialNum
0, 1, 2 and Home
predictions would have serialNum
3, 4, 5 respectively.
The predictions won't always be 3 per category, they can be any number.
Any advice is appreciated.
Based on your comments, the problem you want to solve is:
Allow the user to use the arrow keys to change which item is selected in a list, which is split across three categories.
I'm going to be using three components:
<Categories>
to render an array of categories<Category>
to render a single category (array of predictions)<Prediction>
to render a single prediction.It's not that it's not possible, it's that it sets you up to run into strange bugs later on. Try and uniquely identify each prediction with an ID property of its own. Do the same with your categories.
For example:
const prediction = {
id: 1,
name: "Hello, world!"
};
const category = {
id: 1,
name: "First category",
predictions: [prediction]
};
You need to know which predictions are before and after the currently-selected prediction. Rather than messing about with some convoluted logic involving categories, take them out of the equation entirely!
You can use the built-in JS function flatMap
to flatten your categories so you have a big array of predictions to use for your ordering. Then, use React's built-in useState()
to store the currently-selected index.
const Categories = ({ categories }) => {
const allItems = categories.flatMap(x => x.predictions);
const [selectedIndex, setSelectedIndex] = useState(0);
const selectedItem = allItems[selectedIndex];
return (<div>
{categories.map(x => <Category key={x.id} name={x.name} predictions={x.predictions} />}
</div>);
};
Categories
is currently the only component with any knowledge of the selection, so we need a way to tell the child components (the ones that render predictions) whether they're selected or not.
Pass the ID of the current selection down the chain:
// <Categories>
{categories.map(x => <Category
key={x.id}
...
selectedId={selectedItem.id} />)}
The ID is all you need to identify a single prediction. You don't want to pass selectedIndex
, you need allItems
and categories
to make sense of it and there's no point passing all that stuff down.
If you're passing selectedId
down the chain this is nice and easy. At this point you can change the default selection ID to confirm the right predictions are being selected.
const Prediction = ({ id, name, selectedId }) => {
const isSelected = id === selectedId;
return <li className={isSelected ? "highlight" : ""}>{name}</li>;
};
onKeyUp
event to update the stateBind the onKeyUp
event on your <div>
in <Categories>
and wire it up to change selectedIndex
. I've omitted bounds checking for clarity but you definitely want that to avoid going off either end of the array.
const Categories = ({ categories }) => {
...
const [selectedIndex, setSelectedIndex] = useState(0);
const selectedItem = allItems[selectedIndex];
const onKeyUp = (event) => {
switch (event.code) {
case "ArrowUp":
setSelectedIndex(selectedIndex - 1);
break;
case "ArrowDown":
setSelectedIndex(selectedIndex + 1);
break;
}
};
return (<div onKeyUp={onKeyUp} tabIndex="-1">
...
</div>);
};
tabIndex="-1"
is needed so that the div
can receive keyboard events.
Here's a full example, including bounds checking:
// Example data here.
const teeShirts = [
{ id: 1, name: "Black" },
{ id: 2, name: "Red" },
{ id: 3, name: "Blue" },
];
const accessories = [
{ id: 4, name: "Cool Cap" },
{ id: 5, name: "Fancy Tie" },
{ id: 6, name: "Medallion" },
];
const countries = [
{ id: 7, name: "United Kingdom" },
{ id: 8, name: "United States" },
{ id: 9, name: "Australia" },
];
const categories = [
{ id: 1, name: "T-Shirts", predictions: teeShirts },
{ id: 2, name: "Accessories", predictions: accessories },
{ id: 3, name: "Countries", predictions: countries },
];
const Categories = ({ categories }) => {
const allItems = categories.flatMap((x) => x.predictions);
const [selectedIndex, setSelectedIndex] = React.useState(0);
const selectedItem = allItems[selectedIndex];
const onKeyUp = (event) => {
switch (event.code) {
case "ArrowUp":
setSelectedIndex(Math.max(0, selectedIndex - 1));
break;
case "ArrowDown":
setSelectedIndex(
Math.min(allItems.length - 1, selectedIndex + 1)
);
break;
}
};
return (
<div onKeyUp={onKeyUp} tabIndex={-1}>
{categories.map((category) => (
<Category
key={category.name}
id={category.id}
name={category.name}
predictions={category.predictions}
selectedId={selectedItem.id}
/>
))}
</div>
);
};
const Category = ({ name, predictions, selectedId }) => {
return (
<div>
{name}
<ul role="listbox">
{predictions.map((prediction) => (
<Prediction
key={prediction.name}
id={prediction.id}
name={prediction.name}
selectedId={selectedId}
/>
))}
</ul>
</div>
);
};
const Prediction = ({ id, name, selectedId }) => {
const isSelected = id === selectedId;
return <li className={isSelected ? "highlight" : ""}>{name}</li>;
};
// This is just bootstrapping so the example works
const App = () => {
return <Categories categories={categories} />;
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
.highlight {
color: red;
font-weight: bold;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
<div id="root"></div>