diff --git a/CHANGELOG.md b/CHANGELOG.md index bd5f5c35..d4cea9da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Added `no-exclude` cli flag to disable excludes. - When given in standard library format, additional information now shows up in `incorrect_standard_library_use` missing required parameter errors. - Added new [`mixed_table` lint](https://kampfkarren.github.io/selene/lints/mixed_table.html), which will warn against mixed tables. +- Added new [`roblox_roact_non_exhaustive_deps` lint](https://kampfkarren.github.io/selene/lints/roblox_roact_non_exhaustive_deps.html), which will check for valid dependency arrays. ### Fixed - `string.pack` and `string.unpack` now have proper function signatures in the Lua 5.3 standard library. diff --git a/docs/src/lints/roblox_roact_non_exhaustive_deps.md b/docs/src/lints/roblox_roact_non_exhaustive_deps.md new file mode 100644 index 00000000..568d6f1b --- /dev/null +++ b/docs/src/lints/roblox_roact_non_exhaustive_deps.md @@ -0,0 +1,61 @@ +# roblox_roact_non_exhaustive_deps +## What it does +Checks for valid dependency arrays. Verifies that dependencies are [reactive values](https://react.dev/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values) and that upvalues referenced in hooks with dependency arrays are included in the array. + +## Why this is bad +Hooks that are missing dependencies will read stale values and cause bugs. + +## Example +```lua +local function Component(props) + React.useEffect(function() + print(props) + end) -- ok + + React.useEffect(function() + print(props) + end, {}) -- Missing `props` + + React.useEffect(function() + print(props.a) + end, { props.a }) -- ok + + React.useEffect(function() + print(props.a) + end, { props.a.b }) -- Missing `props.a` + + React.useEffect(function() + print(props.a()) + end, { props.a() }) -- Too complex, extract `props.a()` to variable + + local a1 = props.a() + React.useEffect(function() + print(a1) + end, { a1 }) -- now ok + + React.useEffect(function() + print(props[a]) + end, { props[a] }) -- Too complex, extract `props[a]` to variable + + local a2 = props[a] + React.useEffect(function() + print(a2) + end, { a2 }) -- now ok + + React.useEffect(function() + print(props) + end, helperFunction(props)) -- ok +end +``` + +## Remarks +1. This lint assumes either Roact or React is defined. [`undefined_variable`](./undefined_variable.md) will still lint, however. +2. This lint assumes the hook is prefixed with either `React`, `Roact`, `hooks` for legacy Roact, or a variable assigned to them. +3. This lint only takes effect for `useEffect`, `useMemo`, `useCallback`, and `useLayoutEffect`. Custom hooks that take dependency arrays will not lint. +4. This lint does not take effect if nothing is passed to the second argument of the hook. +5. This lint is only active if you are using the Roblox standard library. +6. This lint warns against complex dependency expressions like function calls and dynamic indexing. This currently false negatives with function calls without any indexing, such as `{ a() }`. + +## Deviations from [eslint-plugin-react-hooks/exhaustive-deps](https://www.npmjs.com/package/eslint-plugin-react-hooks) +1. ESLint requires passing in `a` for `a.b()` since js can implicitly pass `a` as `this` to `a.b`. Lua doesn't do this, so we allow `a.b` as dependencies. +2. ESLint complains about brackets in dependencies like `a["b"]` as being too complex. If string literals are inside the brackets and not a variable like `a[b]`, we recognize that and treat it the same as `a.b`.