React Data GridFeedback Cell Type - Step-by-Step Guide (React)
- Overview
- What You'll Build
- Complete Example
- Prerequisites
- Step 1: Import Dependencies
- Step 2: Create the Editor Component
- Step 3: Add Styling
- Step 4: Read Config from Cell Properties
- Step 5: Add Keyboard Shortcuts
- Step 6: Complete Editor Component
- Step 7: Use in Handsontable
- How It Works - Complete Flow
- Enhancements
- Accessibility
- Performance Considerations
- TypeScript Support
- Best Practices
Overview
This guide shows how to create a feedback editor cell using emoji buttons (π, π, π€·) with React's EditorComponent. The example uses a feature-roadmap table with a Feedback column; the editor supports quick selection, keyboard navigation (Arrow keys, Tab), and per-column configuration via external CSS.
Difficulty: Beginner Time: ~15 minutes Libraries: None
What You'll Build
A cell that:
- Displays emoji feedback buttons (π, π, π€·) when editing
- Shows the selected emoji when viewing
- Supports keyboard navigation (Arrow Left/Right and Tab to cycle options)
- Provides click-to-select and closes the editor on choice
- Uses React's
EditorComponentwith render prop and external CSS (e.g.feedback-editorclass) - Reads per-column options from
cellProperties.configinonPrepare
Complete Example
Prerequisites
npm install @handsontable/react-wrapper
What you need:
- React 16.8+ (hooks support)
@handsontable/react-wrapperpackage- Basic React knowledge (hooks, JSX)
Step 1: Import Dependencies
import { useState, useEffect, useCallback, ComponentProps } from 'react';
import { HotTable, HotColumn, EditorComponent } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
registerAllModules();
What we're importing:
EditorComponent- React component for creating custom editorsHotTableandHotColumn- React wrapper components- React hooks for state management
- Handsontable styles
Step 2: Create the Editor Component
Create a React component that uses EditorComponent with the render prop pattern.
type EditorComponentProps = ComponentProps<typeof EditorComponent<string>>;
const FeedbackEditor = () => {
const [config, setConfig] = useState<string[]>(['π', 'π', 'π€·']);
return (
<EditorComponent<string>>
{({ value, setValue, finishEditing }) => (
<div className="editor">
{config.map((item) => (
<button
key={item}
className={`button ${value === item ? 'active' : ''}`}
onClick={() => {
setValue(item);
finishEditing();
}}
>
{item}
</button>
))}
</div>
)}
</EditorComponent>
);
};
What's happening:
EditorComponentwraps your editor UI- The
childrenprop is a function that receives editor state value- Current editor valuesetValue- Function to update the valuefinishEditing- Function to save and close the editor- Render buttons for each option in the config
- Highlight the active button based on current value
Key concepts:
- Render prop pattern:
EditorComponentuses a function as children - State management:
valueandsetValueare provided byEditorComponent - React components: Use standard React patterns (JSX, className, onClick)
Step 3: Add Styling
Style the editor container and buttons using CSS or inline styles.
const FeedbackEditor = () => {
const [config, setConfig] = useState<string[]>(['π', 'π', 'π€·']);
return (
<EditorComponent<string>>
{({ value, setValue, finishEditing }) => (
<>
<style>{`
.editor {
box-sizing: border-box;
display: flex;
gap: 3px;
padding: 3px;
background: rgb(238, 238, 238);
border: 1px solid rgb(204, 204, 204);
border-radius: 4px;
height: 100%;
width: 100%;
}
.button.active {
background: #007bff;
color: white;
}
.button:hover {
background: #f0f0f0;
}
.button {
background: #fff;
color: black;
border: none;
padding: 0;
margin: 0;
height: 100%;
width: 100%;
font-size: 16px;
font-weight: bold;
text-align: center;
cursor: pointer;
}
`}</style>
<div className="editor">
{config.map((item, _index, _array) => (
<button
key={item}
className={`button ${value === item ? 'active' : ''}`}
onClick={() => {
setValue(item);
finishEditing();
}}
style={{
width: `${100 / _array.length}%`
}}
>
{item}
</button>
))}
</div>
</>
)}
</EditorComponent>
);
};
What's happening:
- Container uses flexbox for horizontal button layout
- Buttons dynamically size based on config length
- Active button has blue background
- Hover effects for better UX
Key styling:
display: flex- Horizontal button layoutgap: 3px- Space between buttonswidth: ${100 / _array.length}%- Dynamic button width.activeclass - Highlights selected button
Step 4: Read Config from Cell Properties
Use onPrepare to read per-column configuration.
const FeedbackEditor = () => {
const [config, setConfig] = useState<string[]>(['π', 'π', 'π€·']);
const onPrepare: EditorComponentProps['onPrepare'] = (
_row,
_column,
_prop,
_TD,
_originalValue,
cellProperties
) => {
// Read config from column definition
if (cellProperties.config) {
setConfig(cellProperties.config as string[]);
}
};
return (
<EditorComponent<string> onPrepare={onPrepare}>
{({ value, setValue, finishEditing }) => (
// ... editor UI
)}
</EditorComponent>
);
};
What's happening:
onPrepareis called before the editor openscellPropertiescontains column-specific configuration- Read
configfromcellProperties.config - Update state to reflect column-specific options
Why this matters:
- Different columns can have different options
- One editor component, multiple configurations
- Dynamic options based on column settings
Step 5: Add Keyboard Shortcuts
Add keyboard navigation using the shortcuts prop.
const FeedbackEditor = () => {
const [config, setConfig] = useState<string[]>(['π', 'π', 'π€·']);
const [shortcuts, setShortcuts] = useState<EditorComponentProps['shortcuts']>([]);
const getNextValue = useCallback((value: string) => {
const index = config.indexOf(value);
return index === config.length - 1 ? config[0] : config[index + 1];
}, [config]);
const getPrevValue = useCallback((value: string) => {
const index = config.indexOf(value);
return index === 0 ? config[config.length - 1] : config[index - 1];
}, [config]);
useEffect(() => {
setShortcuts([
{
keys: [['ArrowRight'], ['Tab']],
callback: ({ value, setValue }, _event) => {
setValue(getNextValue(value));
return false; // Prevent default Tab behavior
}
},
{
keys: [['ArrowLeft']],
callback: ({ value, setValue }, _event) => {
setValue(getPrevValue(value));
}
}
]);
}, [config, getNextValue, getPrevValue]);
return (
<EditorComponent<string> shortcuts={shortcuts}>
{({ value, setValue, finishEditing }) => (
// ... editor UI
)}
</EditorComponent>
);
};
What's happening:
- ArrowRight/Tab: Move to next option (wraps to first if at end)
- ArrowLeft: Move to previous option (wraps to last if at start)
callbackreceives{ value, setValue, finishEditing }as first parameter- Return
falseto prevent default behavior (e.g., Tab moving to next cell)
Keyboard navigation benefits:
- Fast selection without mouse
- Accessible for keyboard-only users
- Intuitive left/right navigation
- Tab cycles through options instead of moving cells
Step 6: Complete Editor Component
Put it all together:
type EditorComponentProps = ComponentProps<typeof EditorComponent<string>>;
const FeedbackEditor = () => {
const [config, setConfig] = useState<string[]>(['π', 'π', 'π€·']);
const [shortcuts, setShortcuts] = useState<EditorComponentProps['shortcuts']>([]);
const onPrepare: EditorComponentProps['onPrepare'] = (
_row,
_column,
_prop,
_TD,
_originalValue,
cellProperties
) => {
if (cellProperties.config) {
setConfig(cellProperties.config as string[]);
}
};
const getNextValue = useCallback((value: string) => {
const index = config.indexOf(value);
return index === config.length - 1 ? config[0] : config[index + 1];
}, [config]);
const getPrevValue = useCallback((value: string) => {
const index = config.indexOf(value);
return index === 0 ? config[config.length - 1] : config[index - 1];
}, [config]);
useEffect(() => {
setShortcuts([
{
keys: [['ArrowRight'], ['Tab']],
callback: ({ value, setValue }, _event) => {
setValue(getNextValue(value));
return false;
}
},
{
keys: [['ArrowLeft']],
callback: ({ value, setValue }, _event) => {
setValue(getPrevValue(value));
}
}
]);
}, [config, getNextValue, getPrevValue]);
return (
<EditorComponent<string> onPrepare={onPrepare} shortcuts={shortcuts}>
{({ value, setValue, finishEditing }) => (
<>
<style>{`
.editor {
box-sizing: border-box;
display: flex;
gap: 3px;
padding: 3px;
background: rgb(238, 238, 238);
border: 1px solid rgb(204, 204, 204);
border-radius: 4px;
height: 100%;
width: 100%;
}
.button.active:hover,
.button.active {
background: #007bff;
color: white;
}
.button:hover {
background: #f0f0f0;
}
.button {
background: #fff;
color: black;
border: none;
padding: 0;
margin: 0;
height: 100%;
width: 100%;
font-size: 16px;
font-weight: bold;
text-align: center;
cursor: pointer;
}
`}</style>
<div className="editor">
{config.map((item, _index, _array) => (
<button
key={item}
className={`button ${value === item ? 'active' : ''}`}
onClick={() => {
setValue(item);
finishEditing();
}}
style={{
width: `${100 / _array.length}%`
}}
>
{item}
</button>
))}
</div>
</>
)}
</EditorComponent>
);
};
What's happening:
- State management:
configandshortcutsmanaged with React hooks - onPrepare: Reads column-specific config
- shortcuts: Keyboard navigation handlers
- Render prop: Renders buttons based on config
- Styling: CSS-in-JS for editor appearance
Step 7: Use in Handsontable
Use the editor component in your HotTable:
const ExampleComponent = () => {
return (
<HotTable
autoRowSize={true}
rowHeaders={true}
autoWrapRow={true}
licenseKey="non-commercial-and-evaluation"
height="auto"
data={data}
colHeaders={true}
>
<HotColumn
width={250}
editor={FeedbackEditor}
config={['π', 'π', 'π€·']}
data="feedback"
title="Feedback"
/>
<HotColumn
width={250}
editor={FeedbackEditor}
config={['1', '2', '3', '4', '5']}
data="stars"
title="Rating (1-5)"
/>
</HotTable>
);
};
What's happening:
editor={FeedbackEditor}- Assigns the editor component to the columnconfig={['π', 'π', 'π€·']}- Column-specific options- Same editor component, different configurations per column
Key features:
- Reusable editor component
- Per-column configuration
- Type-safe with TypeScript
How It Works - Complete Flow
- Initial Render: Cell displays the emoji value (π, π, or π€·)
- User Double-Clicks or Enter: Editor opens,
onPreparereads column config - Editor Opens:
EditorComponentpositions container over cell - Button Display: All options visible, current value highlighted
- User Interaction:
- Click a button β
setValue(item)andfinishEditing()called - Press ArrowLeft/Right β Shortcut callback updates value
- Press Tab β Cycles through options (prevents default cell navigation)
- Click a button β
- Visual Feedback: Selected button highlighted in blue
- User Confirms: Press Enter, click button, or click away
- Save: Value saved to cell
- Editor Closes: Cell shows selected emoji
Enhancements
1. Custom Renderer with Styling
Add a custom renderer to style the emoji display:
import { rendererFactory } from 'handsontable/renderers';
const cellDefinition = {
renderer: rendererFactory(({ td, value }) => {
td.innerHTML = `
<div style="text-align: center; font-size: 1.5em; padding: 4px;">
${value || 'π€·'}
</div>
`;
})
};
// Use in HotColumn
<HotColumn
editor={FeedbackEditor}
renderer={cellDefinition.renderer}
config={['π', 'π', 'π€·']}
data="feedback"
/>
What's happening:
- Center-aligns the emoji
- Increases font size for better visibility
- Adds padding for spacing
2. More Feedback Options
Add more emoji options:
<HotColumn
editor={FeedbackEditor}
config={['π', 'π', 'π€·', 'β€οΈ', 'π₯', 'β']}
data="feedback"
/>
The editor automatically adjusts button widths based on config length.
3. Custom Button Styling
Enhanced button appearance with CSS:
<style>{`
.button {
padding: 8px;
border: 2px solid #ddd;
background: white;
color: #333;
border-radius: 4px;
cursor: pointer;
font-size: 1.2em;
transition: all 0.2s;
}
.button.active {
border-color: #007bff;
background: #007bff;
color: white;
}
.button:hover {
transform: scale(1.05);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
`}</style>
4. Dynamic Config from Cell Properties
The onPrepare hook already handles this! Just pass different configs:
<HotColumn
editor={FeedbackEditor}
config={['π', 'π', 'β€οΈ', 'π₯']}
data="feedback"
/>
5. Tooltip on Hover
Add tooltips to buttons:
{config.map((item) => {
const tooltips: Record<string, string> = {
'π': 'Positive feedback',
'π': 'Negative feedback',
'π€·': 'Neutral feedback'
};
return (
<button
key={item}
className={`button ${value === item ? 'active' : ''}`}
onClick={() => {
setValue(item);
finishEditing();
}}
title={tooltips[item] || ''}
>
{item}
</button>
);
})}
6. Text Labels Instead of Emojis
Use text buttons for clarity:
<HotColumn
editor={FeedbackEditor}
config={['Positive', 'Negative', 'Neutral']}
data="feedback"
/>
The editor works with any string values, not just emojis.
7. Using External CSS File
Move styles to a separate CSS file:
/* feedback-editor.css */
.editor {
box-sizing: border-box;
display: flex;
gap: 3px;
padding: 3px;
background: rgb(238, 238, 238);
border: 1px solid rgb(204, 204, 204);
border-radius: 4px;
height: 100%;
width: 100%;
}
.button.active {
background: #007bff;
color: white;
}
.button:hover {
background: #f0f0f0;
}
.button {
background: #fff;
color: black;
border: none;
padding: 0;
margin: 0;
height: 100%;
width: 100%;
font-size: 16px;
font-weight: bold;
text-align: center;
cursor: pointer;
}
import './feedback-editor.css';
const FeedbackEditor = () => {
// ... component code without <style> tag
};
Accessibility
React buttons are inherently accessible, but you can enhance them:
{config.map((item, index) => (
<button
key={item}
className={`button ${value === item ? 'active' : ''}`}
onClick={() => {
setValue(item);
finishEditing();
}}
aria-label={`${item} feedback option`}
aria-pressed={value === item}
tabIndex={value === item ? 0 : -1}
>
{item}
</button>
))}
Keyboard navigation:
- Tab: Navigate to editor (focuses active button)
- Arrow Left/Right: Cycle through options (via shortcuts)
- Enter: Select current option and finish editing
- Escape: Cancel editing
- Click: Direct selection
ARIA attributes:
aria-label: Describes each buttonaria-pressed: Indicates selected statetabIndex: Controls keyboard focus order
Performance Considerations
Why This Is Fast
- React Virtual DOM: Efficient updates only when value changes
- No External Libraries: Zero overhead beyond React
- Efficient Re-renders: Only re-renders when config or value changes
- Native Events: Browser-optimized click handlers
React Hooks Optimization
The useCallback and useEffect hooks ensure shortcuts are only recreated when config changes:
const getNextValue = useCallback((value: string) => {
const index = config.indexOf(value);
return index === config.length - 1 ? config[0] : config[index + 1];
}, [config]); // Only recreate if config changes
useEffect(() => {
setShortcuts([...]);
}, [config, getNextValue, getPrevValue]); // Only update when dependencies change
TypeScript Support
EditorComponent is fully typed. You can specify the value type:
<EditorComponent<string>>
{({ value, setValue, finishEditing }) => {
// TypeScript knows value is string | undefined
// TypeScript knows setValue accepts string
return (
// ... editor UI
);
}}
</EditorComponent>
For number-based feedback:
<EditorComponent<number>>
{({ value, setValue, finishEditing }) => {
// TypeScript knows value is number | undefined
return (
// ... editor UI
);
}}
</EditorComponent>
Best Practices
- Use
onPreparefor per-cell configuration - AccesscellPropertiesto read custom options - Handle keyboard events properly - Use shortcuts for navigation
- Call
finishEditing()appropriately - When user confirms changes (Enter, blur, button click) - Keep render prop function simple - Extract complex logic into separate components or hooks
- Use
useCallbackfor helper functions - Prevents unnecessary re-renders - Update shortcuts in
useEffect- Ensures shortcuts match current config
Congratulations! You've created a simple feedback editor with emoji buttons using React's EditorComponent, perfect for quick feedback selection in your data grid!