Skip to content

No Derived State

Suggestion
small-rules/no-derived-state

Disallow storing derived state in an effect.

Rationale

Ported from eslint-plugin-react-you-might-not-need-an-effect/no-derived-state. The upstream rule points at the “Updating state based on props or state” section of the React docs, so that’s where the intent comes from.

This catches state that’s just a copy of something you could compute: an effect sets state whose value comes from props or other state. The copy only refreshes when the effect runs, and the effect only runs after render, so you’re permanently one render behind whatever it’s derived from.

Just compute it during render. If the computation is expensive, memoize it, but the common case on Roblox is a cheap calculation and the memo is the overhead. Killing the state copy also kills the whole class of “why is my label stale” bugs.

Diagnostic Messages

avoidDerivedState
Avoid storing derived state. Instead, compute "{{state}}" directly during render.

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-derived-state": [
"error",
{
"environment": "roblox-ts"
}
]
}
}

Examples

Derived state stored by an effect
import React, { useEffect, useState } from "@rbxts/react";
export function Form(): React.Element {
const [firstName, setFirstName] = useState("Taylor");
const [lastName, setLastName] = useState("Swift");
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
return (
<frame>
<textlabel Text={fullName} />
<textbox
Text={firstName}
TextChanged={(textbox: { readonly Text: string }) => setFirstName(textbox.Text)}
/>
<textbox Text={lastName} TextChanged={(textbox: { readonly Text: string }) => setLastName(textbox.Text)} />
</frame>
);
}
Value derived during render
import React, { useState } from "@rbxts/react";
export function Form(): React.Element {
// oxlint-disable-next-line no-unused-vars, sonar/no-unused-vars, small-rules/no-dead-store -- The setter stays unused; the sample only derives the full name.
const [firstName, setFirstName] = useState("Taylor");
// oxlint-disable-next-line no-unused-vars, sonar/no-unused-vars, small-rules/no-dead-store -- The setter stays unused; the sample only derives the full name.
const [lastName, setLastName] = useState("Swift");
const fullName = `${firstName} ${lastName}`;
return <textlabel Text={fullName} />;
}