How to use web components in React with Typescript

How to use web components in React with Typescript

When building a Laravel Inertia React application with TypeScript, I encountered an issue where custom web components were not recognized by TypeScript, even after importing and defining their types. This post documents how I resolved this issue for my Rich Text Editor component, but the solution applies to any custom web component.

The Problem

TypeScript does not natively recognize custom HTML elements like . To fix this, we need to extend the JSX.IntrinsicElements interface to include our custom component and define its valid attributes.

Solution: Extending JSX.IntrinsicElements

Add the following declaration to your project to define the custom component and its attributes:


declare global {
    namespace JSX {
        interface IntrinsicElements {
            "rich-text-editor": {
                placeholder?: string;
                class?: string;
                ref?: React.Ref<HTMLElement>;
            };
        }
    }
}

In the above code I have used my “rich-text-editor” component and also defined the valid attributes. If you don’t want to enforce strict type checking for the custom component’s attributes, you can use any instead. However, this approach sacrifices type safety and is not recommended for production code.

declare global {
    namespace JSX {
        interface IntrinsicElements {
            "rich-text-editor": any;
        }
    }
}

Using the Custom Component in React

After defining the custom component, you can use it in your React code. Below is an example of a wrapper component for the :

// RichTextEditorWrapper.tsx
import React, { useRef, useEffect, forwardRef } from 'react';

import './rich-text-editor.js';


interface RichTextEditorProps {
    value?: string;
    onChange?: (value: string) => void;
    placeholder?: string;
    className?: string;
}

// This wrapper allows React to communicate with your custom element
const RichTextEditorWrapper = forwardRef<HTMLElement, RichTextEditorProps>(
    ({ value, onChange, placeholder, className }, ref) => {
        const editorRef = useRef<HTMLElement | null>(null);

        useEffect(() => {
            // Get reference to the actual DOM element
            const editor = editorRef.current;
            if (!editor) return;

            // Set initial value
            if (value !== undefined && editor.innerHTML !== value) {
                editor.innerHTML = value;
            }

            // Add event listener for content changes
            const handleContentChange = (event: Event) => {
                const customEvent = event as CustomEvent;
                if (onChange && customEvent.detail) {
                    onChange(customEvent.detail.value);
                }
            };

            // Listen for custom events your web component might dispatch
            editor.addEventListener('content-change', handleContentChange);

            return () => {
                // Cleanup
                editor.removeEventListener('content-change', handleContentChange);
            };
        }, [value, onChange]);

        return (
            <rich-text-editor
                ref={(el) => {
                    editorRef.current = el;
                    if (typeof ref === 'function') ref(el);
                    else if (ref) ref.current = el;
                }}
                placeholder={placeholder}
                class={className}
            />
        );
    }
);

export default RichTextEditorWrapper;

Example Usage

Here’s how you can use the RichTextEditorWrapper in a React application:

import React, { useState } from 'react';
import RichTextEditorWrapper from './RichTextEditorWrapper';

const App = () => {
    const [content, setContent] = useState('');

    const handleChange = (value: string) => {
        setContent(value);
    };

    return (
        <div>
            <RichTextEditorWrapper
                value={content}
                onChange={handleChange}
                placeholder="Enter your text here"
                className="editor"
            />
            <div>
                <h3>Preview:</h3>
                <div dangerouslySetInnerHTML={{ __html: content }} />
            </div>
        </div>
    );
};

export default App;

Restart the TypeScript Server

After making these changes, you may need to restart the TypeScript server for the changes to take effect. In Visual Studio Code, press Ctrl + Shift + P (or Cmd + Shift + P on macOS), type “Restart TypeScript Server,” and select the option from the dropdown.

Common Errors and Fixes

  • Error: Property 'rich-text-editor' does not exist on type 'JSX.IntrinsicElements'
    • Ensure the declare global block is correctly added to your project and that the TypeScript server has been restarted.
  • Error: Cannot find module './rich-text-editor.js'
    • Ensure the path to the custom component is correct and that the file exists.

Conclusion

By extending the JSX.IntrinsicElements interface, you can seamlessly integrate custom web components into your TypeScript-based React application. This approach ensures type safety and improves developer experience. If you encounter any issues or have suggestions for improvement, feel free to leave a comment below. Happy coding!

A full stack developer learning a developing web applications since my university time for more than 20 years now and Pega Certified Lead System Architect (since 2013) with nearly 16 years experience in Pega.

Leave a Reply

Your email address will not be published. Required fields are marked *

Back To Top