Commit e4928fe837 for handsontable.com

commit e4928fe837fc3aff6e3802052b5b487e286a8ad9
Author: Szymon Dolnik <42141039+Haxikowy@users.noreply.github.com>
Date:   Mon May 18 10:55:01 2026 +0200

    DEV-1693: Fix React example in multi-column filter panel recipe (#12588)

    * DEV-1693: Fix React example in multi-column filter panel recipe

    * DEV-1693: Refactor React filter panel example to use uncontrolled refs

    Switch from controlled state + useEffect to uncontrolled refs so filter
    changes never trigger a HotTable re-render that could reset plugin state.
    Extract a FilterLabel component to remove repeated label/input markup.

    ---------

    Co-authored-by: Szymon Dolnik <szymon.dolnik@handsontable.com>

diff --git a/docs/content/recipes/filtering-search/multi-column-filter-panel/react/example1.css b/docs/content/recipes/filtering-search/multi-column-filter-panel/react/example1.css
index 89ec2e446e..c0fa7cf6e2 100644
--- a/docs/content/recipes/filtering-search/multi-column-filter-panel/react/example1.css
+++ b/docs/content/recipes/filtering-search/multi-column-filter-panel/react/example1.css
@@ -14,7 +14,3 @@
   gap: 0.25rem;
   min-width: 120px;
 }
-
-.example-controls-container .filter-label--wide {
-  min-width: 160px;
-}
diff --git a/docs/content/recipes/filtering-search/multi-column-filter-panel/react/example1.jsx b/docs/content/recipes/filtering-search/multi-column-filter-panel/react/example1.jsx
index 3a5a6af0f4..b4b013e04e 100644
--- a/docs/content/recipes/filtering-search/multi-column-filter-panel/react/example1.jsx
+++ b/docs/content/recipes/filtering-search/multi-column-filter-panel/react/example1.jsx
@@ -1,4 +1,4 @@
-import { useRef, useState } from 'react';
+import { useRef } from 'react';
 import { HotTable } from '@handsontable/react-wrapper';
 import { registerAllModules } from 'handsontable/registry';
 import './example1.css';
@@ -20,20 +20,31 @@ const sourceData = [
 ];
 /* end:skip-in-preview */

+const FilterLabel = ({ label, children }) => (
+  <label className="filter-label">
+    {label}
+    {children}
+  </label>
+);
+
 const ExampleComponent = () => {
   const hotRef = useRef(null);
-  const [nameFilter, setNameFilter] = useState('');
-  const [categoryFilter, setCategoryFilter] = useState('');
-  const [minPrice, setMinPrice] = useState('');
-  const [maxPrice, setMaxPrice] = useState('');
+  const nameRef = useRef(null);
+  const categoryRef = useRef(null);
+  const minPriceRef = useRef(null);
+  const maxPriceRef = useRef(null);

-  function applyFilters(name, category, min, max) {
+  const applyFilters = () => {
     const hot = hotRef.current?.hotInstance;

     if (!hot) {
       return;
     }

+    const name = nameRef.current?.value ?? '';
+    const category = categoryRef.current?.value ?? '';
+    const minPrice = minPriceRef.current?.value ?? '';
+    const maxPrice = maxPriceRef.current?.value ?? '';
     const filtersPlugin = hot.getPlugin('filters');

     filtersPlugin.clearConditions();
@@ -46,21 +57,21 @@ const ExampleComponent = () => {
       filtersPlugin.addCondition(0, 'contains', [name.trim()]);
     }

-    if (min && max) {
-      const lowerBound = Number(min);
-      const upperBound = Number(max);
+    if (minPrice && maxPrice) {
+      const lowerBound = Number(minPrice);
+      const upperBound = Number(maxPrice);

       if (Number.isFinite(lowerBound) && Number.isFinite(upperBound)) {
         filtersPlugin.addCondition(2, 'between', [lowerBound, upperBound]);
       }
-    } else if (min) {
-      const lowerBound = Number(min);
+    } else if (minPrice) {
+      const lowerBound = Number(minPrice);

       if (Number.isFinite(lowerBound)) {
         filtersPlugin.addCondition(2, 'gte', [lowerBound]);
       }
-    } else if (max) {
-      const upperBound = Number(max);
+    } else if (maxPrice) {
+      const upperBound = Number(maxPrice);

       if (Number.isFinite(upperBound)) {
         filtersPlugin.addCondition(2, 'lte', [upperBound]);
@@ -68,72 +79,31 @@ const ExampleComponent = () => {
     }

     filtersPlugin.filter();
-    hot.render();
-  }
-
-  function handleNameChange(e) {
-    const value = e.target.value;
-
-    setNameFilter(value);
-    applyFilters(value, categoryFilter, minPrice, maxPrice);
-  }
-
-  function handleCategoryChange(e) {
-    const value = e.target.value;
-
-    setCategoryFilter(value);
-    applyFilters(nameFilter, value, minPrice, maxPrice);
-  }
+  };

-  function handleMinPriceChange(e) {
-    const value = e.target.value;
+  const clearFilters = () => {
+    if (nameRef.current) nameRef.current.value = '';
+    if (categoryRef.current) categoryRef.current.value = '';
+    if (minPriceRef.current) minPriceRef.current.value = '';
+    if (maxPriceRef.current) maxPriceRef.current.value = '';

-    setMinPrice(value);
-    applyFilters(nameFilter, categoryFilter, value, maxPrice);
-  }
-
-  function handleMaxPriceChange(e) {
-    const value = e.target.value;
-
-    setMaxPrice(value);
-    applyFilters(nameFilter, categoryFilter, minPrice, value);
-  }
-
-  function clearFilters() {
-    setNameFilter('');
-    setCategoryFilter('');
-    setMinPrice('');
-    setMaxPrice('');
-
-    const hot = hotRef.current?.hotInstance;
-
-    if (!hot) {
-      return;
-    }
-
-    const filtersPlugin = hot.getPlugin('filters');
-
-    filtersPlugin.clearConditions();
-    filtersPlugin.filter();
-    hot.render();
-  }
+    applyFilters();
+  };

   return (
     <div>
       <div className="example-controls-container">
         <div className="filter-panel">
-          <label className="filter-label filter-label--wide">
-            Product name
+          <FilterLabel label="Product name">
             <input
+              ref={nameRef}
               type="text"
               placeholder="Contains..."
-              value={nameFilter}
-              onChange={handleNameChange}
+              onChange={applyFilters}
             />
-          </label>
-          <label className="filter-label filter-label--wide">
-            Category
-            <select value={categoryFilter} onChange={handleCategoryChange}>
+          </FilterLabel>
+          <FilterLabel label="Category">
+            <select ref={categoryRef} onChange={applyFilters}>
               <option value="">All categories</option>
               <option value="Bikes">Bikes</option>
               <option value="Safety">Safety</option>
@@ -141,27 +111,25 @@ const ExampleComponent = () => {
               <option value="Accessories">Accessories</option>
               <option value="Maintenance">Maintenance</option>
             </select>
-          </label>
-          <label className="filter-label">
-            Min price
+          </FilterLabel>
+          <FilterLabel label="Min price">
             <input
+              ref={minPriceRef}
               type="number"
               min="0"
               placeholder="0"
-              value={minPrice}
-              onChange={handleMinPriceChange}
+              onChange={applyFilters}
             />
-          </label>
-          <label className="filter-label">
-            Max price
+          </FilterLabel>
+          <FilterLabel label="Max price">
             <input
+              ref={maxPriceRef}
               type="number"
               min="0"
               placeholder="2500"
-              value={maxPrice}
-              onChange={handleMaxPriceChange}
+              onChange={applyFilters}
             />
-          </label>
+          </FilterLabel>
           <button type="button" onClick={clearFilters}>
             Clear all filters
           </button>
diff --git a/docs/content/recipes/filtering-search/multi-column-filter-panel/react/example1.tsx b/docs/content/recipes/filtering-search/multi-column-filter-panel/react/example1.tsx
index 13aa5da4fe..2e3ef1281f 100644
--- a/docs/content/recipes/filtering-search/multi-column-filter-panel/react/example1.tsx
+++ b/docs/content/recipes/filtering-search/multi-column-filter-panel/react/example1.tsx
@@ -1,4 +1,4 @@
-import { useRef, useState } from 'react';
+import { useRef, type ReactNode } from 'react';
 import { HotTable, HotTableRef } from '@handsontable/react-wrapper';
 import { registerAllModules } from 'handsontable/registry';
 import './example1.css';
@@ -20,20 +20,36 @@ const sourceData = [
 ];
 /* end:skip-in-preview */

+type FilterLabelProps = {
+  label: string;
+  children: ReactNode;
+};
+
+const FilterLabel = ({ label, children }: FilterLabelProps) => (
+  <label className="filter-label">
+    {label}
+    {children}
+  </label>
+);
+
 const ExampleComponent = () => {
   const hotRef = useRef<HotTableRef>(null);
-  const [nameFilter, setNameFilter] = useState('');
-  const [categoryFilter, setCategoryFilter] = useState('');
-  const [minPrice, setMinPrice] = useState('');
-  const [maxPrice, setMaxPrice] = useState('');
+  const nameRef = useRef<HTMLInputElement>(null);
+  const categoryRef = useRef<HTMLSelectElement>(null);
+  const minPriceRef = useRef<HTMLInputElement>(null);
+  const maxPriceRef = useRef<HTMLInputElement>(null);

-  function applyFilters(name: string, category: string, min: string, max: string) {
+  const applyFilters = () => {
     const hot = hotRef.current?.hotInstance;

     if (!hot) {
       return;
     }

+    const name = nameRef.current?.value ?? '';
+    const category = categoryRef.current?.value ?? '';
+    const minPrice = minPriceRef.current?.value ?? '';
+    const maxPrice = maxPriceRef.current?.value ?? '';
     const filtersPlugin = hot.getPlugin('filters');

     filtersPlugin.clearConditions();
@@ -46,21 +62,21 @@ const ExampleComponent = () => {
       filtersPlugin.addCondition(0, 'contains', [name.trim()]);
     }

-    if (min && max) {
-      const lowerBound = Number(min);
-      const upperBound = Number(max);
+    if (minPrice && maxPrice) {
+      const lowerBound = Number(minPrice);
+      const upperBound = Number(maxPrice);

       if (Number.isFinite(lowerBound) && Number.isFinite(upperBound)) {
         filtersPlugin.addCondition(2, 'between', [lowerBound, upperBound]);
       }
-    } else if (min) {
-      const lowerBound = Number(min);
+    } else if (minPrice) {
+      const lowerBound = Number(minPrice);

       if (Number.isFinite(lowerBound)) {
         filtersPlugin.addCondition(2, 'gte', [lowerBound]);
       }
-    } else if (max) {
-      const upperBound = Number(max);
+    } else if (maxPrice) {
+      const upperBound = Number(maxPrice);

       if (Number.isFinite(upperBound)) {
         filtersPlugin.addCondition(2, 'lte', [upperBound]);
@@ -68,72 +84,31 @@ const ExampleComponent = () => {
     }

     filtersPlugin.filter();
-    hot.render();
-  }
-
-  function handleNameChange(e: React.ChangeEvent<HTMLInputElement>) {
-    const value = e.target.value;
-
-    setNameFilter(value);
-    applyFilters(value, categoryFilter, minPrice, maxPrice);
-  }
-
-  function handleCategoryChange(e: React.ChangeEvent<HTMLSelectElement>) {
-    const value = e.target.value;
-
-    setCategoryFilter(value);
-    applyFilters(nameFilter, value, minPrice, maxPrice);
-  }
-
-  function handleMinPriceChange(e: React.ChangeEvent<HTMLInputElement>) {
-    const value = e.target.value;
-
-    setMinPrice(value);
-    applyFilters(nameFilter, categoryFilter, value, maxPrice);
-  }
-
-  function handleMaxPriceChange(e: React.ChangeEvent<HTMLInputElement>) {
-    const value = e.target.value;
-
-    setMaxPrice(value);
-    applyFilters(nameFilter, categoryFilter, minPrice, value);
-  }
+  };

-  function clearFilters() {
-    setNameFilter('');
-    setCategoryFilter('');
-    setMinPrice('');
-    setMaxPrice('');
+  const clearFilters = () => {
+    if (nameRef.current) nameRef.current.value = '';
+    if (categoryRef.current) categoryRef.current.value = '';
+    if (minPriceRef.current) minPriceRef.current.value = '';
+    if (maxPriceRef.current) maxPriceRef.current.value = '';

-    const hot = hotRef.current?.hotInstance;
-
-    if (!hot) {
-      return;
-    }
-
-    const filtersPlugin = hot.getPlugin('filters');
-
-    filtersPlugin.clearConditions();
-    filtersPlugin.filter();
-    hot.render();
-  }
+    applyFilters();
+  };

   return (
     <div>
       <div className="example-controls-container">
         <div className="filter-panel">
-          <label className="filter-label filter-label--wide">
-            Product name
+          <FilterLabel label="Product name">
             <input
+              ref={nameRef}
               type="text"
               placeholder="Contains..."
-              value={nameFilter}
-              onChange={handleNameChange}
+              onChange={applyFilters}
             />
-          </label>
-          <label className="filter-label filter-label--wide">
-            Category
-            <select value={categoryFilter} onChange={handleCategoryChange}>
+          </FilterLabel>
+          <FilterLabel label="Category">
+            <select ref={categoryRef} onChange={applyFilters}>
               <option value="">All categories</option>
               <option value="Bikes">Bikes</option>
               <option value="Safety">Safety</option>
@@ -141,27 +116,25 @@ const ExampleComponent = () => {
               <option value="Accessories">Accessories</option>
               <option value="Maintenance">Maintenance</option>
             </select>
-          </label>
-          <label className="filter-label">
-            Min price
+          </FilterLabel>
+          <FilterLabel label="Min price">
             <input
+              ref={minPriceRef}
               type="number"
               min="0"
               placeholder="0"
-              value={minPrice}
-              onChange={handleMinPriceChange}
+              onChange={applyFilters}
             />
-          </label>
-          <label className="filter-label">
-            Max price
+          </FilterLabel>
+          <FilterLabel label="Max price">
             <input
+              ref={maxPriceRef}
               type="number"
               min="0"
               placeholder="2500"
-              value={maxPrice}
-              onChange={handleMaxPriceChange}
+              onChange={applyFilters}
             />
-          </label>
+          </FilterLabel>
           <button type="button" onClick={clearFilters}>
             Clear all filters
           </button>