Skip to content

No Chain State Updates

Suggestion
small-rules/no-chain-state-updates

Disallow chaining state changes in an effect.

Rationale

Ported from eslint-plugin-react-you-might-not-need-an-effect/no-chain-state-updates. The upstream rule points at the “Chains of computations” section of the React docs.

This flags an effect that depends on state and then writes different state. State A changes, the effect runs, it sets state B, you render again. That’s a chain, and chains are how one input change becomes four renders. If B happens to be in another effect’s dependencies, the chain keeps going.

The chain exists because someone wanted a computed value but reached for an effect instead of computing during render. Set both states where the change starts, or derive the follow-on value during render and skip that extra step. A fetch-then-set chain can be on purpose, so treat it as a nudge to look rather than a verdict.

Diagnostic Messages

avoidChainingStateUpdates
Avoid chaining state changes. When possible, update "{{state}}" along with other relevant state simultaneously.

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

Examples

State update chained through an effect
import React, { useEffect, useState } from "@rbxts/react";
export function Game(): React.Element {
const [round, setRound] = useState(1);
const [isGameOver, setIsGameOver] = useState(false);
useEffect(() => {
if (round > 10) {
setIsGameOver(true);
}
}, [round]);
return (
<frame>
<textlabel Text={isGameOver} />
<textbutton Text={round} onActivated={() => setRound(round + 1)} />
</frame>
);
}
Related state updated together
import React, { useState } from "@rbxts/react";
export function Game(): React.Element {
const [round, setRound] = useState(1);
const [isGameOver, setIsGameOver] = useState(false);
function handleRoundComplete(): void {
const nextRound = round + 1;
setRound(nextRound);
if (nextRound > 10) {
setIsGameOver(true);
}
}
return (
<frame>
<textlabel Text={isGameOver} />
<textbutton Text={round} Event={{ Activated: handleRoundComplete }} />
</frame>
);
}