Search code examples
javascriptreactjsarrayssearcharray-map

In Javascript, how do I get the serial number of an item in Array?


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>
        );
    });
};

Expectation:

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.


Solution

  • 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.

    Avoid using the array index as an ID

    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]
    };
    

    Flatten the hierarchy to make things easier

    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>);
    };
    

    Tell your child components which prediction is selected

    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.

    Tell your predictions how to highlight

    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>;
    };
    

    Bind the onKeyUp event to update the state

    Bind 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.

    Putting it all together

    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>