<div id="app"></div>
body {
  padding: 20px;
}

.input-group {
  width: 300px;
}

.search-group {
  display: flex;
  height: 38px;
}

ul, ol {
  list-style: none;
}

.fw-bolder {
  padding-top: 24px;
  padding-left: 0;
}
import { useEffect, useState, memo, useMemo, useCallback } from "https://esm.sh/react";
import ReactDOM from "https://esm.sh/react-dom";

const fruits = [
  'apple',
  'banana',
  'coconut',
  'dragon fruit',
  'eggplant',
  'fig'
];

const shuffle = (array) => {
  for(let i=0; i<array.length-1; i++) {
    const randomIndex = Math.floor(Math.random() * array.length);
    [array[i], array[randomIndex]] = [array[randomIndex], array[i]];
  }
  // console.log('array', array)
  return array;
};

const Search = memo(({onChange}) => {
  console.log('Search rendered');
  return (
    <div class="input-group mb-3">
      <input
        type="text"
        class="form-control"
        placeholder="Type your fruits"
        onChange={(e) => onChange(e.target.value)} />
    </div>
  );
})

function App() {
  const [allfruits, setAllFruits] = useState(fruits);
  // NOT USING USECALLBACK
  //  const handleSearch = (keywords) => {
  //   setAllFruits(fruits.filter(fruit => fruit.toLowerCase().includes(keywords.toLowerCase())));
  // };
  
  //USING USECALLBACK
  const handleSearch = useCallback((keywords) => {
    setAllFruits(fruits.filter(fruit => fruit.toLowerCase().includes(keywords.toLowerCase())));
  }, []);
  
  return (
    <>
      <div class="search-group">
        <Search onChange={handleSearch} />
        {/*記得解構以免共享參考導致渲染出錯*/}
        <button class="btn btn-outline-secondary" type="button" onClick={() => setAllFruits([...shuffle(allfruits)])}>Shuffle</button>
      </div>
      <ol class="fw-bolder">
        {allfruits.map(fruit => <li>{fruit.charAt(0).toUpperCase() + fruit.slice(1)}</li>)}
       </ol>
  </>)
  
}

ReactDOM.render(<App />, document.querySelector("#app"));
View Compiled

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.0.2/css/bootstrap.min.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js