Skip to content

feat(eslint-plugin): add prefer-useloader rule#3698

Open
travisbreaks wants to merge 1 commit intopmndrs:masterfrom
travisbreaks:feat/prefer-useloader
Open

feat(eslint-plugin): add prefer-useloader rule#3698
travisbreaks wants to merge 1 commit intopmndrs:masterfrom
travisbreaks:feat/prefer-useloader

Conversation

@travisbreaks
Copy link
Copy Markdown

@travisbreaks travisbreaks commented Mar 16, 2026

Summary

Implements the prefer-useloader rule from the ESLint plugin RFC (#2701).

  • Flags .load() and .loadAsync() calls inside useEffect/useLayoutEffect
  • Recommends useLoader for automatic Suspense integration and caching
  • Added to both recommended and all configs
  • 10 test cases (5 valid, 5 invalid), all passing
  • Docs with incorrect/correct examples

This picks up from where #2724 left off (closed unmerged). No overlap with the no-clone-in-loop / no-new-in-loop rules already landed in #2710.

Why prefer useLoader?

Calling Loader.load() or Loader.loadAsync() inside effects bypasses React Suspense boundaries and the built-in caching that useLoader provides. This is one of the most common R3F pitfalls, documented in the pitfalls guide.

Test plan

  • All 21 existing + new tests pass (yarn test in eslint-plugin)
  • Codegen ran clean (configs, index, README updated)
  • Rule correctly ignores .load()/.loadAsync() outside effects
  • Rule catches violations in both useEffect and useLayoutEffect
  • Multiple violations in a single effect reported individually

Partial close of #2701.

Co-Authored-By: Claude Opus 4.6 (1M context) tadao@travisfixes.com

Prefer R3F's useLoader hook over Loader.load()/loadAsync() inside effects
for automatic suspense integration and caching.

Closes pmndrs#2701 (partial)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codesandbox-ci
Copy link
Copy Markdown

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 43ffeac:

Sandbox Source
example Configuration

@DennisSmolek
Copy link
Copy Markdown
Member

The problem is that many dynamic systems use effects or other things to load textures later in runtime. Triggering suspense might switch to a fallback causing a blink or even a entire section to unload depending on where the boundry is placed.
Also this wouldnt catch useCallback functions called within the effect. I would need to see a test of loading something that takes some time dynamically and if it unloads the existing asset while waiting.

@travisbreaks
Copy link
Copy Markdown
Author

Friendly ping. Let me know if this needs any changes or if there's anything blocking review.

@travisbreaks
Copy link
Copy Markdown
Author

Good point. useLoader triggers Suspense, so for dynamic runtime loading (swapping textures mid-scene, progressively loading based on interaction), suspending could unmount existing content while the new asset loads.

The rule targets the common case where someone loads at mount time via useEffect out of habit. For dynamic loading, an eslint-disable makes sense.

Worth adding a "When not to use" section to the docs? Or would a rule option like allowDynamicLoading be more useful?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants