|
| 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 | |
0 commit comments