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>