Skip to content

Commit 165f487

Browse files
Merge pull request #160 from laststance/chore/lifecycle-hooks
Refactor renderer effects into explicit lifecycle hooks
2 parents 4e315a4 + 7c8089d commit 165f487

28 files changed

Lines changed: 411 additions & 83 deletions

eslint.config.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import laststanceReactNextPlugin from '@laststance/react-next-eslint-plugin'
22
import tsPrefixer from 'eslint-config-ts-prefixer'
3+
import reactHooks from 'eslint-plugin-react-hooks'
34
import reactYouMightNotNeedAnEffect from 'eslint-plugin-react-you-might-not-need-an-effect'
45
import { defineConfig } from 'eslint/config'
56

@@ -82,6 +83,15 @@ export default defineConfig([
8283
'react-you-might-not-need-an-effect/no-derived-state': 'error',
8384
},
8485
},
86+
{
87+
files: ['src/**/*.{ts,tsx}'],
88+
plugins: {
89+
'react-hooks': reactHooks,
90+
},
91+
rules: {
92+
'react-hooks/rules-of-hooks': 'error',
93+
},
94+
},
8595
{
8696
plugins: {
8797
'@laststance/react-next': laststanceReactNextPlugin,

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"electron-vite": "^5.0.0",
8585
"eslint": "^10.3.0",
8686
"eslint-config-ts-prefixer": "^4.2.0",
87+
"eslint-plugin-react-hooks": "^7.1.1",
8788
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.1",
8889
"fallow": "2.69.0",
8990
"husky": "^9.1.7",

pnpm-lock.yaml

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/renderer/settings/sections/About.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { match } from 'ts-pattern'
44

55
import { Button } from '@/renderer/src/components/ui/button'
66
import { Separator } from '@/renderer/src/components/ui/separator'
7-
import { useComponentEffect } from '@/renderer/src/hooks/useComponentEffect'
7+
import { useCycleEffect } from '@/renderer/src/hooks/useCycleEffect'
88
import { SKILLS_DESKTOP_REPOSITORY_URL } from '@/shared/constants'
99

1010
import { SectionFrame } from './SectionFrame'
@@ -69,7 +69,7 @@ export const About = React.memo(function About(): React.ReactElement {
6969
const isUpdaterAvailable = Boolean(updateApi)
7070
const [checkStatus, setCheckStatus] = useState<CheckStatus>({ kind: 'idle' })
7171

72-
useComponentEffect(() => {
72+
useCycleEffect(() => {
7373
if (!updateApi) return
7474
const cleanups = [
7575
updateApi.onChecking(() => setCheckStatus({ kind: 'checking' })),

src/renderer/settings/sections/Agents.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React, { useCallback } from 'react'
33

44
import { Button } from '@/renderer/src/components/ui/button'
55
import { Checkbox } from '@/renderer/src/components/ui/checkbox'
6-
import { useComponentEffect } from '@/renderer/src/hooks/useComponentEffect'
6+
import { useCycleEffect } from '@/renderer/src/hooks/useCycleEffect'
77
import { useUpdateSettings } from '@/renderer/src/hooks/useUpdateSettings'
88
import { cn, toggleArrayMember } from '@/renderer/src/lib/utils'
99
import { useAppDispatch, useAppSelector } from '@/renderer/src/redux/hooks'
@@ -44,7 +44,7 @@ export const Agents = React.memo(function Agents(): React.ReactElement {
4444
const hiddenAgentIds = useAppSelector(selectHiddenAgentIds)
4545
const updateSettings = useUpdateSettings()
4646

47-
useComponentEffect(() => {
47+
useCycleEffect(() => {
4848
// Deliberately keyed on `agents.length` only (NOT `loading`). The
4949
// length value transitions 0 → N exactly once per mount lifecycle,
5050
// so the effect dispatches once and never re-fires within a mount.

src/renderer/settings/sections/Appearance.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React from 'react'
22

33
import { Button } from '@/renderer/src/components/ui/button'
4-
import { useComponentEffect } from '@/renderer/src/hooks/useComponentEffect'
4+
import { useUnmountEffect } from '@/renderer/src/hooks/useUnmountEffect'
5+
import { useUpdateEffect } from '@/renderer/src/hooks/useUpdateEffect'
56
import { useUpdateSettings } from '@/renderer/src/hooks/useUpdateSettings'
67
import { useAppSelector } from '@/renderer/src/redux/hooks'
78
import {
@@ -108,16 +109,14 @@ export const Appearance = React.memo(function Appearance(): React.ReactElement {
108109
})
109110
}, [clearPersistTimer, updateSettings])
110111

111-
useComponentEffect(() => {
112+
useUpdateEffect(() => {
112113
clearPersistTimer()
113114
setBlurRadiusDraft(windowBackgroundBlurRadius)
114115
}, [windowBackgroundBlurRadius, clearPersistTimer])
115116

116-
useComponentEffect(() => {
117-
return () => {
118-
clearPersistTimer()
119-
}
120-
}, [clearPersistTimer])
117+
useUnmountEffect(() => {
118+
clearPersistTimer()
119+
})
121120

122121
const blurRadiusLabel =
123122
blurRadiusDraft === WINDOW_BACKGROUND_BLUR_MIN_RADIUS

src/renderer/settings/sections/General.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
ToggleGroup,
88
ToggleGroupItem,
99
} from '@/renderer/src/components/ui/toggle-group'
10-
import { useComponentEffect } from '@/renderer/src/hooks/useComponentEffect'
10+
import { useInitialEffect } from '@/renderer/src/hooks/useInitialEffect'
1111
import { useUpdateSettings } from '@/renderer/src/hooks/useUpdateSettings'
1212
import { useAppSelector } from '@/renderer/src/redux/hooks'
1313
import { TERMINAL_APP_IDS, TERMINAL_APP_UI_LABELS } from '@/shared/constants'
@@ -129,7 +129,7 @@ export const General = React.memo(function General(): React.ReactElement {
129129
const [isMainWindowAvailable, setIsMainWindowAvailable] =
130130
useState<boolean>(true)
131131

132-
useComponentEffect(() => {
132+
useInitialEffect(() => {
133133
let cancelled = false
134134
// Attach `.catch` explicitly — `void promise.then(...)` only suppresses
135135
// TypeScript's "promise not handled" hint, it does NOT silence a real
@@ -149,7 +149,7 @@ export const General = React.memo(function General(): React.ReactElement {
149149
return () => {
150150
cancelled = true
151151
}
152-
}, [])
152+
})
153153

154154
/**
155155
* Capture the live main-window bounds via IPC and persist them.

src/renderer/src/components/dashboard/DashboardCanvas.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import ReactGridLayout, {
44
type Layout,
55
} from 'react-grid-layout'
66

7-
import { useComponentEffect } from '@/renderer/src/hooks/useComponentEffect'
7+
import { useCycleEffect } from '@/renderer/src/hooks/useCycleEffect'
88
import { useAppDispatch, useAppSelector } from '@/renderer/src/redux/hooks'
99
import {
1010
seedDefaultsIfEmpty,
@@ -50,7 +50,7 @@ export const DashboardCanvas = React.memo(
5050

5151
// Seed the default 4 pages on first render. The reducer guards against
5252
// re-seeding (`initialized` flag), so calling this repeatedly is free.
53-
useComponentEffect(() => {
53+
useCycleEffect(() => {
5454
if (!isInitialized) dispatch(seedDefaultsIfEmpty())
5555
}, [dispatch, isInitialized])
5656

src/renderer/src/components/dashboard/DashboardPageTabs.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
DropdownMenuSeparator,
1010
DropdownMenuTrigger,
1111
} from '@/renderer/src/components/ui/dropdown-menu'
12-
import { useComponentEffect } from '@/renderer/src/hooks/useComponentEffect'
12+
import { useCycleEffect } from '@/renderer/src/hooks/useCycleEffect'
1313
import { cn } from '@/renderer/src/lib/utils'
1414
import { useAppDispatch, useAppSelector } from '@/renderer/src/redux/hooks'
1515
import {
@@ -185,7 +185,7 @@ const PageTab = React.memo(function PageTab({
185185

186186
// Reset the draft to the latest canonical name each time rename mode opens.
187187
// Using rAF defers focus until after the input is mounted in the DOM.
188-
useComponentEffect(() => {
188+
useCycleEffect(() => {
189189
if (isRenaming) {
190190
setDraftName(page.name)
191191
requestAnimationFrame(() => renameInputRef.current?.select())

src/renderer/src/components/dashboard/widgets/LeaderboardWidget.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AlertCircle, type LucideIcon } from 'lucide-react'
22
import React from 'react'
33

4-
import { useComponentEffect } from '@/renderer/src/hooks/useComponentEffect'
4+
import { useCycleEffect } from '@/renderer/src/hooks/useCycleEffect'
55
import { useAppDispatch, useAppSelector } from '@/renderer/src/redux/hooks'
66
import { loadLeaderboard } from '@/renderer/src/redux/slices/marketplaceSlice'
77
import type { RankingFilter } from '@/shared/types'
@@ -48,7 +48,7 @@ export const LeaderboardWidget = React.memo(function LeaderboardWidget({
4848
(state) => state.marketplace.leaderboard[filter],
4949
)
5050

51-
useComponentEffect(() => {
51+
useCycleEffect(() => {
5252
dispatch(loadLeaderboard(filter))
5353
}, [dispatch, filter])
5454

0 commit comments

Comments
 (0)