Skip to content

This tutorial shows you how to build an emoji feedback cell in React using Handsontable’s EditorComponent, with per-column configuration and keyboard navigation.

JavaScript
import { useState, useEffect, useCallback } from 'react';
import { HotTable, HotColumn, EditorComponent } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import './example1.css';
// register Handsontable's modules
registerAllModules();
/* start:skip-in-preview */
export const data = [
{ feature: 'Dark Mode', category: 'UI', priority: 'High', feedback: '👍', votes: 124, status: 'Planned' },
{ feature: 'Bulk Edit', category: 'Core', priority: 'High', feedback: '👍', votes: 98, status: 'In Progress' },
{ feature: 'AI Suggestions', category: 'Beta', priority: 'Medium', feedback: '🤷', votes: 45, status: 'Research' },
{ feature: 'Offline Mode', category: 'Infra', priority: 'Low', feedback: '👎', votes: 12, status: 'Backlog' },
];
/* end:skip-in-preview */
export const FeedbackEditor = () => {
const [config, setConfig] = useState(['👍', '👎', '🤷']);
const [shortcuts, setShortcuts] = useState([]);
const onPrepare = (_row, _column, _prop, _TD, _originalValue, cellProperties) => {
setConfig(cellProperties.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]);
const getNextValue = useCallback(
(value) => {
const index = config.indexOf(value);
return index === config.length - 1 ? config[0] : config[index + 1];
},
[config]
);
const getPrevValue = useCallback(
(value) => {
const index = config.indexOf(value);
return index === 0 ? config[config.length - 1] : config[index - 1];
},
[config]
);
return (
<EditorComponent onPrepare={onPrepare} shortcuts={shortcuts}>
{({ value, setValue, finishEditing }) => (
<>
<div className="feedback-editor">
{config.map((item, _index, _array) => (
<button
className={value === item ? 'active' : ''}
key={item}
onClick={() => {
setValue(item);
finishEditing();
}}
style={{
width: `${100 / _array.length}%`,
}}
>
{item}
</button>
))}
</div>
</>
)}
</EditorComponent>
);
};
const ExampleComponent = () => {
return (
<HotTable
autoRowSize={true}
rowHeaders={true}
autoWrapRow={true}
licenseKey="non-commercial-and-evaluation"
height="auto"
width="100%"
data={data}
colHeaders={['Feature', 'Category', 'Priority', 'Feedback', 'Votes', 'Status']}
headerClassName="htLeft"
>
<HotColumn data="feature" type="text" width={200} />
<HotColumn data="category" type="text" width={90} />
<HotColumn data="priority" type="text" width={100} />
<HotColumn data="feedback" type="text" width={100} editor={FeedbackEditor} config={['👍', '👎', '🤷']} />
<HotColumn data="votes" type="numeric" width={60} />
<HotColumn data="status" type="text" width={120} />
</HotTable>
);
};
export default ExampleComponent;
TypeScript
import { useState, useEffect, ComponentProps, useCallback } from 'react';
import { HotTable, HotColumn, EditorComponent } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import './example1.css';
// register Handsontable's modules
registerAllModules();
/* start:skip-in-preview */
export const data = [
{ feature: 'Dark Mode', category: 'UI', priority: 'High', feedback: '👍', votes: 124, status: 'Planned' },
{ feature: 'Bulk Edit', category: 'Core', priority: 'High', feedback: '👍', votes: 98, status: 'In Progress' },
{ feature: 'AI Suggestions', category: 'Beta', priority: 'Medium', feedback: '🤷', votes: 45, status: 'Research' },
{ feature: 'Offline Mode', category: 'Infra', priority: 'Low', feedback: '👎', votes: 12, status: 'Backlog' },
];
/* end:skip-in-preview */
type EditorComponentProps = ComponentProps<typeof EditorComponent<string>>;
export const FeedbackEditor = () => {
const [config, setConfig] = useState<string[]>(['👍', '👎', '🤷']);
const [shortcuts, setShortcuts] = useState<EditorComponentProps['shortcuts']>([]);
const onPrepare: EditorComponentProps['onPrepare'] = (_row, _column, _prop, _TD, _originalValue, cellProperties) => {
setConfig(cellProperties.config as string[]);
};
useEffect(() => {
setShortcuts([
{
keys: [['ArrowRight'], ['Tab']],
callback: ({value, setValue}, _event) => {
setValue(getNextValue(value));
return false;
}
},
{
keys: [['ArrowLeft']],
callback: ({value, setValue}, _event) => {
setValue(getPrevValue(value));
}
}
])
}, [config]);
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]);
return (
<EditorComponent<string> onPrepare={onPrepare} shortcuts={shortcuts}>
{({ value, setValue, finishEditing }) => (
<>
<div className="feedback-editor">
{config.map((item, _index, _array) => (
<button
className={value === item ? 'active' : ''}
key={item}
onClick={() => {
setValue(item);
finishEditing();
}}
style={{
width: 100 / _array.length + '%'
}}
>
{item}
</button>
))}
</div>
</>
)}
</EditorComponent>
);
};
const ExampleComponent = () => {
return (
<HotTable
autoRowSize={true}
rowHeaders={true}
autoWrapRow={true}
licenseKey="non-commercial-and-evaluation"
height="auto"
width="100%"
data={data}
colHeaders={['Feature', 'Category', 'Priority', 'Feedback', 'Votes', 'Status']}
headerClassName="htLeft"
>
<HotColumn data="feature" type="text" width={200} />
<HotColumn data="category" type="text" width={90} />
<HotColumn data="priority" type="text" width={100} />
<HotColumn data="feedback" type="text" width={100} editor={FeedbackEditor} config={['👍', '👎', '🤷']} />
<HotColumn data="votes" type="numeric" width={60} />
<HotColumn data="status" type="text" width={120} />
</HotTable>
);
};
export default ExampleComponent;
CSS
.feedback-editor {
display: flex;
gap: var(--ht-gap, 4px);
width: 100%;
height: 100%;
box-sizing: border-box !important;
padding: var(--ht-cell-vertical-padding, 4px) var(--ht-cell-horizontal-padding, 8px);
background-color: var(--ht-cell-editor-background-color, #ffffff);
box-shadow: inset 0 0 0 var(--ht-cell-editor-border-width, 2px)
var(--ht-cell-editor-border-color, #1a42e8),
0 0 var(--ht-cell-editor-shadow-blur-radius, 0) 0
var(--ht-cell-editor-shadow-color, transparent);
border: none;
border-radius: 0;
}
.feedback-editor button {
background: var(--ht-background-color, #ffffff);
color: var(--ht-foreground-color, #000000);
border: 1px solid var(--ht-border-color, #e0e0e0);
border-radius: var(--ht-border-radius, 4px);
padding: 0;
margin: 0;
height: 100%;
width: 33%;
font-size: var(--ht-font-size, 14px);
text-align: center;
cursor: pointer;
}
.feedback-editor button:hover {
background: var(--ht-border-color, #e0e0e0);
}
.feedback-editor button.active,
.feedback-editor button.active:hover {
background: var(--ht-accent-color, #1a42e8);
color: #ffffff;
border-color: var(--ht-accent-color, #1a42e8);
}

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 EditorComponent with render prop and external CSS (e.g. feedback-editor class)
  • Reads per-column options from cellProperties.config in onPrepare

Prerequisites

Terminal window
npm install @handsontable/react-wrapper

What you need:

  • React 16.8+ (hooks support)
  • @handsontable/react-wrapper package
  • Basic React knowledge (hooks, JSX)
  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 editors
    • HotTable and HotColumn - React wrapper components
    • React hooks for state management
    • Handsontable styles
  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:

    1. EditorComponent wraps your editor UI
    2. The children prop is a function that receives editor state
    3. value - Current editor value
    4. setValue - Function to update the value
    5. finishEditing - Function to save and close the editor
    6. Render buttons for each option in the config
    7. Highlight the active button based on current value

    Key concepts:

    • Render prop pattern: EditorComponent uses a function as children
    • State management: value and setValue are provided by EditorComponent
    • React components: Use standard React patterns (JSX, className, onClick)
  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 layout
    • gap: 3px - Space between buttons
    • width: ${100 / _array.length}% - Dynamic button width
    • .active class - Highlights selected button
  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:

    • onPrepare is called before the editor opens
    • cellProperties contains column-specific configuration
    • Read config from cellProperties.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
  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)
    • callback receives { value, setValue, finishEditing } as first parameter
    • Return false to 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
  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: config and shortcuts managed 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
  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 column
    • config={['👍', '👎', '🤷']} - 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

  1. Initial Render: Cell displays the emoji value (👍, 👎, or 🤷)
  2. User Double-Clicks or Enter: Editor opens, onPrepare reads column config
  3. Editor Opens: EditorComponent positions container over cell
  4. Button Display: All options visible, current value highlighted
  5. User Interaction:
    • Click a button → setValue(item) and finishEditing() called
    • Press ArrowLeft/Right → Shortcut callback updates value
    • Press Tab → Cycles through options (prevents default cell navigation)
  6. Visual Feedback: Selected button highlighted in blue
  7. User Confirms: Press Enter, click button, or click away
  8. Save: Value saved to cell
  9. 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 — 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 button
  • aria-pressed: Indicates selected state
  • tabIndex: Controls keyboard focus order

Performance Considerations

Why This Is Fast

  1. React Virtual DOM: Efficient updates only when value changes
  2. No External Libraries: Zero overhead beyond React
  3. Efficient Re-renders: Only re-renders when config or value changes
  4. 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

  1. Use onPrepare for per-cell configuration - Access cellProperties to read custom options
  2. Handle keyboard events properly - Use shortcuts for navigation
  3. Call finishEditing() appropriately - When user confirms changes (Enter, blur, button click)
  4. Keep render prop function focused - Extract complex logic into separate components or hooks
  5. Use useCallback for helper functions - Prevents unnecessary re-renders
  6. Update shortcuts in useEffect - Ensures shortcuts match current config

What you learned

You built an emoji feedback cell editor in React using Handsontable’s EditorComponent. You used the render prop pattern to render configurable option buttons, onPrepare to read per-column configuration from cellProperties, and the shortcuts prop for keyboard navigation.

Next steps