| name | set-up-modus-event-listeners |
| description | Properly set up and clean up event listeners for Modus web components using useEffect patterns |
Set Up Modus Event Listeners
Properly set up and clean up event listeners for Modus web components following established patterns.
When to Use
Use this skill when:
- Adding event handling to any Modus wrapper component
- Components need to respond to web component events
- You need to sync React state with web component state
- Handling user interactions from Modus components
Pattern Overview
All Modus event listeners follow this pattern:
- Use
useRef to get component reference
- Use
useEffect to set up listeners
- Check for component existence before adding listeners
- Create typed handler functions for each event
- Conditionally attach listeners based on prop existence
- Return cleanup function to remove listeners
Reference Examples
- Simple events:
src/components/ModusCheckbox.tsx:91-150
- Multiple events:
src/components/ModusDropdownMenu.tsx:79-112
- Complex events:
src/components/ModusNavbar.tsx (multiple event handlers)
Basic Template
import { useEffect, useRef } from "react";
import { ModusWcComponent } from "@trimble-oss/moduswebcomponents-react";
export default function ModusComponent({
onEventName,
}: {
onEventName?: (event: CustomEvent<EventDetailType>) => void;
}) {
const componentRef = useRef<HTMLModusWcComponentElement>(null);
useEffect(() => {
const component = componentRef.current;
if (!component) return;
const handleEventName = (event: Event) => {
onEventName?.(event as CustomEvent<EventDetailType>);
};
if (onEventName) {
component.addEventListener("eventName", handleEventName);
}
return () => {
if (onEventName) {
component.removeEventListener("eventName", handleEventName);
}
};
}, [onEventName]);
return <ModusWcComponent ref={componentRef} />;
}
Multiple Event Handlers
export default function ModusComponent({
onInputChange,
onInputFocus,
onInputBlur,
onValueChange,
}: {
onInputChange?: (event: CustomEvent<InputEvent>) => void;
onInputFocus?: (event: CustomEvent<FocusEvent>) => void;
onInputBlur?: (event: CustomEvent<FocusEvent>) => void;
onValueChange?: (event: CustomEvent<boolean>) => void;
}) {
const componentRef = useRef<HTMLModusWcComponentElement>(null);
useEffect(() => {
const component = componentRef.current;
if (!component) return;
const handleInputChange = (event: Event) => {
onInputChange?.(event as CustomEvent<InputEvent>);
};
const handleInputFocus = (event: Event) => {
onInputFocus?.(event as CustomEvent<FocusEvent>);
};
const handleInputBlur = (event: Event) => {
onInputBlur?.(event as CustomEvent<FocusEvent>);
};
const handleValueChange = (event: Event) => {
onValueChange?.(event as CustomEvent<boolean>);
};
if (onInputChange)
component.addEventListener("inputChange", handleInputChange);
if (onInputFocus)
component.addEventListener("inputFocus", handleInputFocus);
if (onInputBlur)
component.addEventListener("inputBlur", handleInputBlur);
if (onValueChange)
component.addEventListener("inputChange", handleValueChange);
return () => {
if (onInputChange)
component.removeEventListener("inputChange", handleInputChange);
if (onInputFocus)
component.removeEventListener("inputFocus", handleInputFocus);
if (onInputBlur)
component.removeEventListener("inputBlur", handleInputBlur);
if (onValueChange)
component.removeEventListener("inputChange", handleValueChange);
};
}, [onInputChange, onInputFocus, onInputBlur, onValueChange]);
return <ModusWcComponent ref={componentRef} />;
}
Key Patterns
1. Null Check First
Always check if component exists:
useEffect(() => {
const component = componentRef.current;
if (!component) return;
}, [dependencies]);
2. Conditional Listener Attachment
Only attach listeners if handlers are provided:
if (onEventName) {
component.addEventListener("eventName", handleEventName);
}
3. Proper Cleanup
Always remove listeners in cleanup:
return () => {
if (onEventName) {
component.removeEventListener("eventName", handleEventName);
}
};
4. Type Casting
Cast events to proper CustomEvent types:
const handleEvent = (event: Event) => {
onEvent?.(event as CustomEvent<EventDetailType>);
};
5. Dependencies Array
Include all handler props in dependencies:
useEffect(() => {
}, [onEventName, onOtherEvent]);
Common Event Names
Modus components use these event names:
| Component | Event Name | Detail Type |
|---|
| Checkbox | inputChange | InputEvent |
| Checkbox | inputFocus | FocusEvent |
| Checkbox | inputBlur | FocusEvent |
| DropdownMenu | itemSelect | { value: string } |
| DropdownMenu | menuVisibilityChange | { isVisible: boolean } |
| Navbar | searchClick | MouseEvent | KeyboardEvent |
| Navbar | mainMenuOpenChange | boolean |
| Accordion | expandedChange | { expanded: boolean; index: number } |
Check Modus documentation for component-specific events.
Advanced Patterns
Event with Side Effects
const handleItemSelect = (event: Event) => {
const customEvent = event as CustomEvent<{ value: string }>;
onItemSelect?.(customEvent);
const dropdown = dropdownRef.current;
if (dropdown) {
dropdown.menuVisible = false;
}
};
Reference: src/components/ModusDropdownMenu.tsx:86-94
Event Value Transformation
const handleValueChange = (event: Event) => {
const customEvent = event as CustomEvent<InputEvent>;
const rawValue = (customEvent.target as HTMLModusWcComponentElement).value;
const transformedValue = transformValue(rawValue);
onValueChange?.(new CustomEvent("valueChange", {
detail: transformedValue,
}));
};
Debounced Events
import { useCallback } from "react";
import { debounce } from "lodash";
useEffect(() => {
const component = componentRef.current;
if (!component) return;
const debouncedHandler = debounce((event: Event) => {
onSearchChange?.(event as CustomEvent<{ value: string }>);
}, 300);
component.addEventListener("searchChange", debouncedHandler);
return () => {
component.removeEventListener("searchChange", debouncedHandler);
debouncedHandler.cancel();
};
}, [onSearchChange]);
Common Mistakes
- Missing cleanup: Always return cleanup function
- Wrong dependencies: Include all handler props in dependency array
- Missing null check: Always check
componentRef.current exists
- Wrong event names: Check Modus docs for exact event names
- Not removing listeners: Must remove in cleanup to prevent memory leaks
- Type casting errors: Use proper CustomEvent types
Testing Checklist
Related Files
src/components/ModusCheckbox.tsx - Basic event handling
src/components/ModusDropdownMenu.tsx - Multiple events
src/components/ModusNavbar.tsx - Complex event handling
.cursor/rules/modus-react-integration.mdc - Integration patterns