Skip to content

Commit be51d4a

Browse files
Copilotgedinakova
andauthored
refactor: add functional component counterparts for 7 class-based samples + README
Agent-Logs-Url: https://github.com/IgniteUI/igniteui-react-examples/sessions/9067d825-3ea4-4e58-aa6d-cf16c15d0b5c Co-authored-by: gedinakova <16817847+gedinakova@users.noreply.github.com>
1 parent 413b7cb commit be51d4a

8 files changed

Lines changed: 956 additions & 0 deletions

File tree

FUNCTIONAL_REFACTORING_README.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Functional Component Refactoring – Summary
2+
3+
This document describes the refactoring of **7 class-based React samples** to **functional components** following modern React best practices (React 16.8+).
4+
5+
Each original `index.tsx` is left unchanged. A new `*Functional.tsx` file has been created alongside it as the functional counterpart.
6+
7+
---
8+
9+
## Why Functional Components?
10+
11+
| Concern | Class component | Functional component |
12+
|---|---|---|
13+
| Boilerplate | Requires `constructor`, `this`, `.bind()` | Plain function, no `this` |
14+
| State | `this.state` + `setState` | `useState` hook |
15+
| Lifecycle | `componentDidMount`, `componentDidUpdate`, `componentWillUnmount` | `useEffect` hook (unified) |
16+
| Refs | `React.createRef()` / callback refs stored on `this` | `useRef` hook |
17+
| Expensive computations | Lazy getter or instance field | `useMemo` hook |
18+
| Stable callbacks | Manual `.bind()` in constructor | `useCallback` hook |
19+
| Code sharing | HOCs / render-props (complex) | Custom hooks (simple) |
20+
21+
---
22+
23+
## Sample 1 – Expansion Panel: Properties & Events
24+
25+
**Files**
26+
- Original: `samples/layouts/expansion-panel/properties-and-events/src/index.tsx`
27+
- Functional: `samples/layouts/expansion-panel/properties-and-events/src/ExpansionPanelPropsAndEventsFunctional.tsx`
28+
29+
**Feature coverage:** `properties`, `event handlers`, `timed state updates`
30+
31+
| Class pattern | Functional equivalent | Why |
32+
|---|---|---|
33+
| `this.state = { subtitleClass, eventSpanClass, eventSpanText }` | Three `useState` calls | Fine-grained state is cleaner and avoids stale state merging pitfalls |
34+
| Manual `.bind()` in constructor | Not needed | Arrow functions and `useCallback` capture the right scope naturally |
35+
| `this.setState(...)` inside `onExpansionPanelClosed` / `onExpansionPanelOpened` | State setter calls inside `useCallback` handlers | State setters are stable—no re-binding needed |
36+
| Duplicated open/close logic | Extracted shared `showEvent` helper (called from both handlers) | Reduces duplication, improves readability |
37+
| `window.clearTimeout(undefined)` (no-op) | Removed (was a bug in original) | Calling `clearTimeout(undefined)` has no effect; correct approach omits it |
38+
39+
---
40+
41+
## Sample 2 – Accordion: Customization
42+
43+
**Files**
44+
- Original: `samples/layouts/accordion/customization/src/index.tsx`
45+
- Functional: `samples/layouts/accordion/customization/src/AccordionCustomizationFunctional.tsx`
46+
47+
**Feature coverage:** `complex state`, `multiple event handlers`, `refs`, `icon registration`
48+
49+
| Class pattern | Functional equivalent | Why |
50+
|---|---|---|
51+
| Private class field `categories` + mutable copy in `setState` | `useState` with immutable update (`prev.map(...)`) | Avoids direct mutation; React can track changes correctly |
52+
| `this.dateTimeInput` callback ref | `useRef<IgrDateTimeInput>` | `useRef` is the idiomatic hook for persistent mutable DOM/component references |
53+
| `this.dateTimeInputRef` callback bound in constructor | `ref={dateTimeInputRef}` with `useRef` | Simpler and no binding required |
54+
| `registerIconFromText` called inside `constructor` | Moved to module scope (runs once on import) | Icon registration is a one-time side effect, not tied to component lifecycle |
55+
| `INITIAL_CATEGORIES` as class field | Extracted as a `const` outside the component | Module-level constants are created once, not on every render |
56+
| Mutable `categoriesCopy[i].checked = ...` then `setState` | Immutable `prev.map(c => ...)` inside `setCategories` | Avoids accidentally mutating React state |
57+
58+
---
59+
60+
## Sample 3 – Stepper: Linear (Multi-step Form)
61+
62+
**Files**
63+
- Original: `samples/layouts/stepper/linear/src/index.tsx`
64+
- Functional: `samples/layouts/stepper/linear/src/LinearStepperFunctional.tsx`
65+
66+
**Feature coverage:** `refs`, `componentDidMount` (event listeners), `component updates`, `form validation state`
67+
68+
| Class pattern | Functional equivalent | Why |
69+
|---|---|---|
70+
| `React.createRef<IgrStepper>()` as class field | `useRef<IgrStepper>` | Hooks-based ref; same semantics, no `this` |
71+
| `componentDidMount` adding native `igcInput` listeners | `useEffect(() => { ... return cleanup }, [linear, checkActiveStepValidity])` | `useEffect` handles both mount *and* re-attachment when dependencies change, with automatic cleanup via the returned function |
72+
| `activeStepIndex` as class field (mutable between renders) | Step index derived inside `checkActiveStepValidity` at call time | Avoids stale-closure issues; derives from the stepper's current DOM state on each call |
73+
| Manual `.bind(this)` for `onSwitchChange` | `useCallback` | Stable reference without manual binding |
74+
| `this.state.linear` accessed inside `onInput` | `linear` passed directly into `checkActiveStepValidity` | Avoids stale closure—the latest value is always passed explicitly |
75+
76+
---
77+
78+
## Sample 4 – Pivot Grid: Features (Data Operations)
79+
80+
**Files**
81+
- Original: `samples/grids/pivot-grid/features/src/index.tsx`
82+
- Functional: `samples/grids/pivot-grid/features/src/PivotGridFeaturesFunctional.tsx`
83+
84+
**Feature coverage:** `data operations`, `complex configuration objects`, `refs`, `aggregate functions`
85+
86+
| Class pattern | Functional equivalent | Why |
87+
|---|---|---|
88+
| Lazy getter with `_pivotConfiguration1` backing field | `useMemo(() => { ... }, [])` | `useMemo` with an empty dependency array is the hook equivalent of a lazy getter—builds once, memoizes forever |
89+
| `_pivotDataFlat` backing field with lazy getter | `useMemo(() => new PivotDataFlat(), [])` | Same pattern—data source created once and kept stable across renders |
90+
| Aggregate functions as `public` instance methods | Promoted to module-level plain functions | Aggregate logic is stateless; module-level functions are garbage-collected-friendly and don't capture `this` |
91+
| `private gridRef(r)` callback ref + `this.setState({})` to trigger re-render | `useRef<IgrPivotGrid>` | Standard hook ref; forced re-render on ref assignment is no longer needed |
92+
| `IgrPivotGridModule` registered in module scope | Unchanged—kept at module scope | Already correct; module registration is a one-time side effect |
93+
94+
---
95+
96+
## Sample 5 – Geo Map: Binding Data from CSV (Async Data Loading)
97+
98+
**Files**
99+
- Original: `samples/maps/geo-map/binding-data-csv/src/index.tsx`
100+
- Functional: `samples/maps/geo-map/binding-data-csv/src/MapBindingDataCSVFunctional.tsx`
101+
102+
**Feature coverage:** `componentDidMount`, `async data fetching`, `refs`, `tooltip rendering`
103+
104+
| Class pattern | Functional equivalent | Why |
105+
|---|---|---|
106+
| `public geoMap: IgrGeographicMap` class field | `useRef<IgrGeographicMap>` | Refs are the correct way to hold mutable, non-render-triggering values in functional components |
107+
| `onMapRef` callback bound in constructor | `ref={geoMapRef}` with `useRef` | Object refs work directly with Ignite UI components; no manual binding |
108+
| `componentDidMount` + `this.onDataLoaded` | `useEffect(() => { fetch(...).then(onDataLoaded) }, [onDataLoaded])` | `useEffect` with an empty/stable dep array runs once after mount, matching `componentDidMount` semantics |
109+
| `this.geoMap` accessed inside `onDataLoaded` | `geoMapRef.current` accessed inside `onDataLoaded` `useCallback` | Reads the latest ref value at call time—no stale reference |
110+
| `createTooltip` as instance method | Promoted to module-level function | Tooltip renderer is stateless and pure; module scope avoids re-creation and `this` |
111+
112+
---
113+
114+
## Sample 6 – Calendar: Disabled Dates (Constructor State Initialization)
115+
116+
**Files**
117+
- Original: `samples/scheduling/calendar/disabled-dates/src/index.tsx`
118+
- Functional: `samples/scheduling/calendar/disabled-dates/src/CalendarDisabledDatesFunctional.tsx`
119+
120+
**Feature coverage:** `properties`, `constructor state initialization`, `stable object references`
121+
122+
| Class pattern | Functional equivalent | Why |
123+
|---|---|---|
124+
| `constructor` computing `disabledDates` and placing it in `this.state` | `useMemo(() => [...], [])` | `useMemo` with an empty dependency array runs once (equivalent to constructor) and memoizes the result, preventing re-creation on every render |
125+
| `this.state.disabledDates` passed as prop | `disabledDates` variable from `useMemo` | Direct variable reference—cleaner and no `this` required |
126+
| Empty `constructor` with `super(props)` | No constructor needed | Functional components have no constructor |
127+
128+
---
129+
130+
## Sample 7 – Tile Manager: Actions (DOM Manipulation + Event Handlers)
131+
132+
**Files**
133+
- Original: `samples/layouts/tile-manager/actions/src/index.tsx`
134+
- Functional: `samples/layouts/tile-manager/actions/src/TileManagerActionsFunctional.tsx`
135+
136+
**Feature coverage:** `event handlers`, `DOM manipulation`, `icon registration`, `class field arrow functions`
137+
138+
| Class pattern | Functional equivalent | Why |
139+
|---|---|---|
140+
| Class field arrow functions (`private onCustomOneClick = (event) => {}`) | `useCallback` | `useCallback` provides stable handler references across renders, preventing unnecessary child re-renders |
141+
| `registerIconFromText` calls in `constructor` | Moved to module scope | Icon registration is a pure side effect; moving it outside the component means it runs exactly once per module load, not once per instance |
142+
| `actionsSlot.parentElement?.querySelectorAll(...)` in original | Simplified to `tile.querySelectorAll('.additional-action')` | The `actionsSlot` traversal was unnecessarily indirect; querying from the tile element is more direct and correct |
143+
| `this.` prefix on all methods and properties | No `this` required | Functions and variables are in lexical scope; arrow functions in `useCallback` close over them naturally |
144+
145+
---
146+
147+
## General Patterns Applied Across All Samples
148+
149+
| Class component pattern | Functional equivalent | Notes |
150+
|---|---|---|
151+
| `extends React.Component<any, any>` | `function ComponentName() {}` | No inheritance needed |
152+
| `constructor(props) { super(props); this.state = {...} }` | `const [x, setX] = useState(...)` per piece of state | Each state slice is independent; no need to spread/merge the full state object |
153+
| `this.setState({ key: value })` | `setKey(value)` | Direct setter; React batches updates automatically in React 18 |
154+
| `public render(): JSX.Element { return (...) }` | Component body `return (...)` | The function body *is* the render method |
155+
| Manual `.bind(this)` in constructor | Not needed (hooks + arrow functions) | Lexical `this` in functional components is never needed |
156+
| `componentDidMount()` | `useEffect(() => { ... }, [])` | Empty dependency array = runs once after first render |
157+
| `componentWillUnmount()` | Cleanup function returned from `useEffect` | Collocates setup and teardown logic |
158+
| Lazy instance getter (backing field + null check) | `useMemo(() => ..., [])` | Memoizes expensive computations; empty deps = computed once |
159+
| Callback ref stored on `this` | `useRef` + `ref={refObj}` | Ref object persists for the component's lifetime |
160+
| `ReactDOM.createRoot(...).render(<Class/>)` | `ReactDOM.createRoot(...).render(<Function/>)` | Unchanged—only the component name changes |
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import React, { useRef, useMemo } from 'react';
2+
import ReactDOM from 'react-dom/client';
3+
import './index.css';
4+
5+
import { IgrPivotGridModule } from 'igniteui-react-grids';
6+
import {
7+
IgrPivotGrid,
8+
IgrPivotConfiguration,
9+
IgrPivotDateDimension,
10+
IgrPivotDimension,
11+
IgrPivotDateDimensionOptions,
12+
SortingDirection,
13+
IgrPivotValue,
14+
IgrPivotAggregator,
15+
} from 'igniteui-react-grids';
16+
import { PivotDataFlat } from './PivotDataFlat';
17+
18+
import 'igniteui-react-grids/grids/themes/light/bootstrap.css';
19+
20+
const mods: any[] = [IgrPivotGridModule];
21+
mods.forEach(m => m.register());
22+
23+
function pivotDataFlatAggregateSumSale(_members: any[], data: any[]): any {
24+
return data.reduce((acc, v) => acc + v.ProductUnitPrice * v.NumberOfUnits, 0);
25+
}
26+
27+
function pivotDataFlatAggregateMinSale(_members: any[], data: any[]): any {
28+
if (data.length === 0) return 0;
29+
const mapped = data.map(x => x.ProductUnitPrice * x.NumberOfUnits);
30+
return mapped.reduce((a, b) => Math.min(a, b));
31+
}
32+
33+
function pivotDataFlatAggregateMaxSale(_members: any[], data: any[]): any {
34+
if (data.length === 0) return 0;
35+
const mapped = data.map(x => x.ProductUnitPrice * x.NumberOfUnits);
36+
return mapped.reduce((a, b) => Math.max(a, b));
37+
}
38+
39+
export default function Sample() {
40+
const gridRef = useRef<IgrPivotGrid>(null);
41+
42+
// useMemo ensures the configuration object is built only once (stable reference)
43+
const pivotConfiguration = useMemo<IgrPivotConfiguration>(() => {
44+
const config = {} as IgrPivotConfiguration;
45+
46+
const dateDimension = new IgrPivotDateDimension();
47+
dateDimension.memberName = 'Date';
48+
dateDimension.enabled = true;
49+
50+
const baseDimension = {} as IgrPivotDimension;
51+
baseDimension.memberName = 'Date';
52+
baseDimension.enabled = true;
53+
dateDimension.baseDimension = baseDimension;
54+
55+
const dateOptions = {} as IgrPivotDateDimensionOptions;
56+
dateOptions.years = true;
57+
dateOptions.months = false;
58+
dateOptions.quarters = true;
59+
dateOptions.fullDate = false;
60+
dateDimension.options = dateOptions;
61+
62+
config.columns = [dateDimension];
63+
64+
const productDim = {} as IgrPivotDimension;
65+
productDim.memberName = 'ProductName';
66+
productDim.sortDirection = SortingDirection.Asc;
67+
productDim.enabled = true;
68+
69+
const cityDim = {} as IgrPivotDimension;
70+
cityDim.memberName = 'SellerCity';
71+
cityDim.enabled = true;
72+
73+
config.rows = [productDim, cityDim];
74+
75+
const sellerDim = {} as IgrPivotDimension;
76+
sellerDim.memberName = 'SellerName';
77+
sellerDim.enabled = true;
78+
79+
config.filters = [sellerDim];
80+
81+
const pivotValue = {} as IgrPivotValue;
82+
pivotValue.member = 'AmountofSale';
83+
pivotValue.displayName = 'Amount of Sale';
84+
pivotValue.enabled = true;
85+
86+
const sumAgg = {} as IgrPivotAggregator;
87+
sumAgg.key = 'SUM';
88+
sumAgg.label = 'Sum of Sale';
89+
sumAgg.aggregator = pivotDataFlatAggregateSumSale;
90+
pivotValue.aggregate = sumAgg;
91+
92+
const sumAgg2 = {} as IgrPivotAggregator;
93+
sumAgg2.key = 'SUM';
94+
sumAgg2.label = 'Sum of Sale';
95+
sumAgg2.aggregator = pivotDataFlatAggregateSumSale;
96+
97+
const minAgg = {} as IgrPivotAggregator;
98+
minAgg.key = 'MIN';
99+
minAgg.label = 'Minimum of Sale';
100+
minAgg.aggregator = pivotDataFlatAggregateMinSale;
101+
102+
const maxAgg = {} as IgrPivotAggregator;
103+
maxAgg.key = 'MAX';
104+
maxAgg.label = 'Maximum of Sale';
105+
maxAgg.aggregator = pivotDataFlatAggregateMaxSale;
106+
107+
pivotValue.aggregateList = [sumAgg2, minAgg, maxAgg];
108+
config.values = [pivotValue];
109+
110+
return config;
111+
}, []);
112+
113+
// useMemo keeps data source instance stable across re-renders
114+
const pivotData = useMemo(() => new PivotDataFlat(), []);
115+
116+
return (
117+
<div className="container sample ig-typography">
118+
<div className="container fill">
119+
<IgrPivotGrid
120+
data={pivotData}
121+
ref={gridRef}
122+
rowSelection="single"
123+
superCompactMode={true}
124+
pivotConfiguration={pivotConfiguration}
125+
/>
126+
</div>
127+
</div>
128+
);
129+
}
130+
131+
// rendering above component to the React DOM
132+
const root = ReactDOM.createRoot(document.getElementById('root'));
133+
root.render(<Sample/>);

0 commit comments

Comments
 (0)