fix: Replace actioningPlugin with useLoading hook#13095
Conversation
EurFelux
left a comment
There was a problem hiding this comment.
Thanks for the fix. I reviewed the diff and the overall approach looks solid: moving plugin action loading state into Redux and reusing it via useLoading should prevent loading-state loss after remount. I did not find blocking correctness or security issues in this change set.
DeJeune
left a comment
There was a problem hiding this comment.
Review Summary
The core idea — persisting plugin loading state in Redux to survive component unmount — is sound and addresses the reported bug well. However, there are a few issues that should be addressed before merging.
Critical / Significant
- Memory leak in
loadingMap: Settingvalue: falseleaves stale keys in the map. The reducer shoulddeletethe key instead. - Unnecessary re-renders:
useLoading()(no-arg) usesuseRuntime()which subscribes to the entireRuntimeState.PluginBrowser(with its virtual list) will re-render on any unrelated runtime state change. UseuseAppSelector((state) => state.runtime.loadingMap)instead. - Namespace collision risk: Session IDs are bare keys while plugin keys use
plugin:prefix. Both share the sameloadingMap. Session keys should also be namespaced (e.g.,session:${id}).
Minor
- Duplicate-click guard added for plugins but not for session deletion — inconsistent.
Positives
- Clean hook API with well-designed function overloads.
- Good
createPluginLoadingKeynamespacing pattern for plugins. - Complete migration — no dangling references to the old
sessionWaiting/setSessionWaitingAction.
|
|
||
| // Debounce search query | ||
| const handleSearchChange = useCallback( | ||
| (value: string) => { |
There was a problem hiding this comment.
Nit: Good that you added duplicate-click guards in handleInstall/handleUninstall. However, using the no-arg useLoading() overload here means the entire component re-renders on any loading state change (even from unrelated features like session deletion).
Since PluginBrowser renders a virtual list with many items, this could be noticeable. An alternative approach: keep actioningPlugin as local state for the UI rendering, but also set the Redux loading key for cross-component persistence. Or refactor to use the single-id overload per action.
There was a problem hiding this comment.
Note
This issue/comment/review was translated by Claude.
Currently, to prevent memory leaks, each call to finishLoading causes the loadingMap reference to change, thereby triggering a re-render.
A better solution would be to not use delete and instead perform periodic GC operations during the application lifecycle. However, since there is currently no lifecycle management architecture, this solution is not being adopted for now.
Original Content
目前为了防止内存泄漏,每次调用finishLoading都会导致loadingMap引用发生变化,从而触发重渲染。
更好的解决方案是不使用delete,同时在应用生命周期内定时执行GC操作。然而,由于目前尚缺少生命周期管理架构,暂不采用此方案。
DeJeune
left a comment
There was a problem hiding this comment.
Follow-up Review
The latest commit addresses both critical issues from the previous review:
- Memory leak fixed —
finishLoadingActionnow usesdelete state.loadingMap[id]instead of settingfalse. Clean. - Re-render issue fixed —
useLoadingnow usesuseAppSelector((state) => state.runtime.loadingMap)instead ofuseRuntime(). Only re-renders whenloadingMapchanges. - Improved API — Splitting
setLoading(value)intostartLoading()/finishLoading()is more expressive and prevents misuse (no way to accidentally pass the wrong boolean).
Two non-blocking nits remain (session key namespacing, verbose JSDoc on internal overload) — can be addressed in a follow-up if desired.
LGTM, approving.
What this PR does
Before this PR:
useState<string | null>) to track loading status inPluginBrowser.tsxsessionWaiting) in Redux storeAfter this PR:
useLoadinghook that manages loading state in Redux store (persists across component lifecycle)setSessionWaitingActionwith more genericsetLoadingActionthat uses aloadingMapfor multiple loading statesPluginBrowser.tsxnow uses the centralizeduseLoadinghook that survives component remountFixes #13092
Why we need it and why it was done in this way
This PR fixes a bug where loading state is lost when the PluginBrowser component unmounts. When the user navigates away and returns, they see "Install" button instead of loading state, leading them to click again and cause duplicate operations.
The following tradeoffs were made:
loadingMap) instead of single session-specific state provides flexibility for future use casesThe following alternatives were considered:
Links to places where the discussion took place:
Breaking changes
If this PR introduces breaking changes, please describe the changes and the impact on users.
Special notes for your reviewer
useLoadinghook supports both global loading map and individual id-based usageChecklist
This checklist is not enforcing, but it's a reminder of items that could be relevant to every PR.
Approvers are expected to review this list.
/gh-pr-review,gh pr diff, or GitHub UI) before requesting review from othersRelease note