skip to Main Content

I have a HTML select drop down list (SortBy component), after selecting an option, the component rerenders, this is not wanted, I want to see the option I selected but it keeps reseting (rerendering) to the first option. I’ve tried useCallback with no luck

When A-Z or Price: Low to high is selected, I want that value retained in the HTML select box but it defaults back to Relevance

function App() {
  const [items, setItems] = useState(products);
  console.log(products);

  const onChange = useCallback((e) => {
      const { value } = e.target;
      let arr = items;

      switch (value) {
        case ("relevance", "saleFirst"):
          arr = saleItemsFirst(items, false);
          break;
        case "ascend":
          arr = sortByName(items);
          break;
        case "price":
          arr = sortByPrice(items);
          break;
  
        default:
          arr = saleItemsFirst(items);
          break;
      }

      setItems([...arr]);
    },
    [items]
  );

  const SortBy = useCallback(() => {
    return (
      <select name="filters" onChange={onChange}>
        <option value="relevance">Relevance</option>
        <option value="ascend">A-Z</option>
        <option value="priceLow">Price: Low to high</option>
        <option value="saleFirst">Sales items first</option>
      </select>
    );
  }, [onChange]);

  return (
    <div className="App">
      <SortBy />
      <hr />
      <section>
        {items.map((val) => (
          <div>
            <span>{val.productName}</span> <div>${val.price}</div>
            <hr />
          </div>
        ))}
      </section>
      ......

2

Answers


  1. The Problem:
    SortBy is getting re-defined whenever items changes because items is in the dependency array for your useCallback, which tells it when to re-run, re-defining the returned function reference. When that happens, it’s a whole new component instance, and it’s being remounted with initial values.

    items has to be in your dependency array if you define a function this way, otherwise, when items changes, your memoized (useCallback’d) component will be stale.

    The Solution:
    Rather than declaring SortBy inside of the App component, you’ll need to declare it outside, and pass in any dependencies it needs as props. That way, the SortBy component will be a module-level constant, and it will only mount once when App mounts.

    Takeaway:
    It’s generally a bad practice to define components inside of other components. Avoid it. The only possible exception are very dimple (like, one-liner) components that have no state concerns. Even then I’d advise defining them outside other components.

    Login or Signup to reply.
  2. Instead of keeping the sorted list items in state you could just keep track of which sort option is selected and memoize the sorted item list, updating it when the selected sort changes:

    // we need a list of items to display
    const items = Array.from({ length: 5 }, (_, i) => `Item ${i + 1}`);
    
    // a mapping of sort names to sort functions
    const sortOptions = {
      ascending: (a, b) => a.localeCompare(b),
      descending: (a, b) => b.localeCompare(a),
      random: () => Math.random() - 0.5,
    }
    
    // the component
    function MyList () {
    
      // keep track of which sort name is selected
      const [sortName, setSortName] = React.useState();
      
      // re-sort the item list when the sort name changes
      const sortedItems = React.useMemo(() => {
        // look up the sort function by name and pass it to array.sort()
        return [...items].sort(sortOptions[sortName]);
      }, [sortName]);
      
      return (
        <div>
          {/* update the sort name on change, and set the value of the select */}
          <select onChange={(e) => setSortName(e.target.value)} value={sortName}>
          
            {/* render an option for each item in sortOptions */}
            {Object.keys(sortOptions).map(sortName => (
              <option key={sortName}>{sortName}</option>
            ))}
            
          </select>
          
          {/* render the list of items */}
          <ol>
            { sortedItems.map(item => <li key={item}>{item}</li>) }
          </ol>
        </div>
      );
    }
    
    
    ReactDOM.render(<MyList />, document.getElementById('root'))
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
    
    
    <div id="root"></div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search