React Data GridFeedback Cell Type - Step-by-Step Guide (React)

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

Complete Example

    Prerequisites

    npm install @handsontable/react-wrapper
    

    What you need:

    • React 16.8+ (hooks support)
    • @handsontable/react-wrapper package
    • 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 editors
    • HotTable and HotColumn - 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:

    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)

    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 layout
    • gap: 3px - Space between buttons
    • width: ${100 / _array.length}% - Dynamic button width
    • .active class - 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:

    • 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

    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)
    • 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

    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: 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

    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 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! 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 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 simple - 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

    Congratulations! You've created a simple feedback editor with emoji buttons using React's EditorComponent, perfect for quick feedback selection in your data grid!