Skip to content

No Event Handler

Suggestion
small-rules/no-event-handler

Disallow using state and an effect as an event handler.

Rationale

Ported from eslint-plugin-react-you-might-not-need-an-effect/no-event-handler. The upstream rule points at the “Sharing logic between event handlers” section of the React docs, so that’s where the intent comes from.

This flags effects whose body is an if-guard around logic that reads state or props. That’s an event handler wearing an effect costume: the condition is the event, the body is the handler. The difference matters because an effect runs after render and only when its dependencies change, so the logic fires late and on a render schedule instead of when the user actually does the thing.

In Roblox the event is a TextChanged, Activated, or similar callback. Put the logic there, or in the parent if it reads props. The rule only catches the if-guard shape though, so it won’t find every disguised handler, it’s a heuristic.

Diagnostic Messages

avoidEventHandler
Avoid using state and effects as an event handler. Instead, call the code that uses "{{name}}" directly when the event occurs.
avoidPropHandler
Avoid using props and effects as an event handler. Instead, move the code that uses "{{name}}" to the parent component.

Configuration

This rule accepts one options object after the severity.

environmentOptional

The React environment: 'roblox-ts' uses @rbxts/react, 'standard' uses react.

{
"jsPlugins": [
"@pobammer-ts/small-rules"
],
"rules": {
"small-rules/no-event-handler": [
"error",
{
"environment": "roblox-ts"
}
]
}
}

Examples

Effect used as an event handler
import React, { useEffect, useState } from "@rbxts/react";
declare const os: {
readonly clock: () => number;
};
declare function submitData(data: { readonly name: string }): void;
export function Form(): React.Element {
const [name, setName] = useState("");
const [dataToSubmit, setDataToSubmit] = useState<{ readonly name: string } | undefined>();
useEffect(() => {
if (dataToSubmit !== undefined && os.clock() % 2 === 0) {
submitData(dataToSubmit);
}
}, [dataToSubmit]);
return (
<frame>
<textbox Text={name} TextChanged={(textbox: { readonly Text: string }) => setName(textbox.Text)} />
<textbutton Text="Submit" Activated={() => setDataToSubmit({ name })} />
</frame>
);
}
Logic called from the event handler
import React, { useState } from "@rbxts/react";
declare function submitData(data: { readonly name: string }): void;
export function Form(): React.Element {
const [name, setName] = useState("");
// oxlint-disable-next-line no-unused-vars, sonar/no-unused-vars, small-rules/no-dead-store -- The setter stays unused; the handler submits the state directly.
const [dataToSubmit, setDataToSubmit] = useState<{ readonly name: string } | undefined>();
function handleSubmit(): void {
if (dataToSubmit !== undefined) {
submitData(dataToSubmit);
}
}
return (
<frame>
<textbox Text={name} TextChanged={(textbox: { readonly Text: string }) => setName(textbox.Text)} />
<textbutton Text="Submit" Activated={handleSubmit} />
</frame>
);
}