Skip to content

Commit 412a3f5

Browse files
Enable more ESLint rules
1 parent b7b59de commit 412a3f5

15 files changed

Lines changed: 149 additions & 65 deletions

File tree

clients/apps/web/eslint.config.mjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { nextJsConfig } from '@polar-sh/eslint-config/next-js'
2+
import pluginQuery from '@tanstack/eslint-plugin-query'
23

34
// Elements replaced by Orbit primitives — ban raw JSX usage as a warning.
45
// const orbitElementRule = (element, replacement) => ({
@@ -16,6 +17,7 @@ import { nextJsConfig } from '@polar-sh/eslint-config/next-js'
1617
/** @type {import("eslint").Linter.Config} */
1718
export default [
1819
...nextJsConfig,
20+
...pluginQuery.configs['flat/recommended'],
1921
{
2022
rules: {
2123
'react-hooks/set-state-in-effect': 'warn',
@@ -24,6 +26,8 @@ export default [
2426
'react-hooks/preserve-manual-memoization': 'warn',
2527
'react-hooks/immutability': 'warn',
2628
'react-hooks/purity': 'warn',
29+
'react-hooks/no-deriving-state-in-effects': 'warn',
30+
'react-hooks/memoized-effect-dependencies': 'warn',
2731
},
2832
},
2933
{
@@ -32,6 +36,8 @@ export default [
3236
'react/no-danger': 'error',
3337
'react/self-closing-comp': 'warn',
3438
'react/jsx-no-useless-fragment': 'warn',
39+
'react/jsx-no-constructed-context-values': 'warn',
40+
'react/no-object-type-as-default-prop': 'warn',
3541
'no-restricted-syntax': [
3642
'error',
3743
{
@@ -51,6 +57,18 @@ export default [
5157
message:
5258
'Do not use style on <Box />. Use design system props instead.',
5359
},
60+
{
61+
selector:
62+
'CallExpression[callee.name="useEffect"] CallExpression[callee.name="fetch"]',
63+
message:
64+
'Do not fetch data inside useEffect. Use TanStack Query (useQuery/useMutation) instead.',
65+
},
66+
{
67+
selector:
68+
'CallExpression[callee.name="useEffect"] CallExpression[callee.object.name="api"]',
69+
message:
70+
'Do not call the API inside useEffect. Use TanStack Query (useQuery/useMutation) instead.',
71+
},
5472
],
5573
'no-restricted-imports': [
5674
'error',

clients/apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
"@polar-sh/typescript-config": "workspace:*",
105105
"@stylexjs/babel-plugin": "^0.18.1",
106106
"@stylexjs/postcss-plugin": "^0.18.1",
107+
"@tanstack/eslint-plugin-query": "^5.94.5",
107108
"@types/big.js": "^6.2.2",
108109
"@types/dom-to-image": "^2.6.7",
109110
"@types/mdx": "^2.0.13",

clients/apps/web/src/app/(main)/[organization]/portal/claim/ClaimPage.tsx

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -57,33 +57,34 @@ export default function ClientPage({
5757
maxWaitingTimeMs: 15000,
5858
})
5959

60-
const claimMutation = useMutation({
61-
mutationFn: async () => {
62-
if (!invitationToken) {
63-
throw new Error('No invitation token')
64-
}
65-
66-
const response = await fetch(
67-
`${CONFIG.BASE_URL}/v1/customer-seats/claim`,
68-
{
69-
method: 'POST',
70-
headers: {
71-
'Content-Type': 'application/json',
60+
const { mutateAsync: claimMutateAsync, error: claimMutationError } =
61+
useMutation({
62+
mutationFn: async () => {
63+
if (!invitationToken) {
64+
throw new Error('No invitation token')
65+
}
66+
67+
const response = await fetch(
68+
`${CONFIG.BASE_URL}/v1/customer-seats/claim`,
69+
{
70+
method: 'POST',
71+
headers: {
72+
'Content-Type': 'application/json',
73+
},
74+
body: JSON.stringify({
75+
invitation_token: invitationToken,
76+
}),
7277
},
73-
body: JSON.stringify({
74-
invitation_token: invitationToken,
75-
}),
76-
},
77-
)
78+
)
7879

79-
if (!response.ok) {
80-
const error = await response.json()
81-
throw new Error(error.detail || 'Failed to claim seat')
82-
}
80+
if (!response.ok) {
81+
const error = await response.json()
82+
throw new Error(error.detail || 'Failed to claim seat')
83+
}
8384

84-
return await response.json()
85-
},
86-
})
85+
return await response.json()
86+
},
87+
})
8788

8889
// Establish SSE connection early to prevent race conditions
8990
const sseReadyRef = useRef(false)
@@ -123,7 +124,7 @@ export default function ClientPage({
123124
sseReadyRef.current = true
124125
}
125126

126-
const result = await claimMutation.mutateAsync()
127+
const result = await claimMutateAsync()
127128

128129
await fulfillmentPromiseRef.current
129130

@@ -138,7 +139,7 @@ export default function ClientPage({
138139
error instanceof Error ? error.message : 'Failed to claim seat'
139140
setClaimError(errorMessage)
140141
}
141-
}, [claimInfo?.product_id, claimMutation, organization.slug, router])
142+
}, [claimInfo?.product_id, claimMutateAsync, organization.slug, router])
142143

143144
if (!invitationToken) {
144145
return (
@@ -242,9 +243,9 @@ export default function ClientPage({
242243
{claimingState !== 'idle' ? 'Claiming...' : 'Claim seat'}
243244
</Button>
244245

245-
{(claimError || claimMutation.error) && (
246+
{(claimError || claimMutationError) && (
246247
<div className="rounded-lg bg-red-50 p-4 text-sm text-red-600 dark:bg-red-900/20 dark:text-red-400">
247-
{claimError || claimMutation.error?.message}
248+
{claimError || claimMutationError?.message}
248249
</div>
249250
)}
250251
</div>

clients/apps/web/src/components/Checkout/Checkout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ const Checkout = ({
178178
)
179179
const distinctId = distinctIdCookie?.split('=')[1]?.trim()
180180

181+
// eslint-disable-next-line no-restricted-syntax -- fire-and-forget analytics, not data fetching
181182
fetch(
182183
getServerURL(`/v1/checkouts/client/${checkout.client_secret}/opened`),
183184
{

clients/apps/web/src/components/Dashboard/DashboardProvider.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { schemas } from '@polar-sh/client'
2-
import { PropsWithChildren, createContext } from 'react'
2+
import { PropsWithChildren, createContext, useMemo } from 'react'
33

44
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
55
interface DashboardContextValue {}
@@ -15,7 +15,11 @@ export const DashboardProvider = ({
1515
}: PropsWithChildren<{
1616
organization: schemas['Organization'] | undefined
1717
}>) => {
18+
const value = useMemo(() => ({}), [])
19+
1820
return (
19-
<DashboardContext.Provider value={{}}>{children}</DashboardContext.Provider>
21+
<DashboardContext.Provider value={value}>
22+
{children}
23+
</DashboardContext.Provider>
2024
)
2125
}

clients/apps/web/src/components/Payouts/PayoutContext.tsx

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { schemas } from '@polar-sh/client'
2-
import React, { createContext, ReactNode, useContext, useState } from 'react'
2+
import React, {
3+
createContext,
4+
ReactNode,
5+
useCallback,
6+
useContext,
7+
useMemo,
8+
useState,
9+
} from 'react'
310

411
interface PayoutContextType {
512
selectedPayout: schemas['Payout'] | null
@@ -19,21 +26,28 @@ export const PayoutProvider: React.FC<{ children: ReactNode }> = ({
1926
>(null)
2027
const [isInvoiceModalOpen, setIsInvoiceModalOpen] = useState(false)
2128

22-
const openInvoiceModal = () => setIsInvoiceModalOpen(true)
23-
const closeInvoiceModal = () => setIsInvoiceModalOpen(false)
29+
const openInvoiceModal = useCallback(() => setIsInvoiceModalOpen(true), [])
30+
const closeInvoiceModal = useCallback(() => setIsInvoiceModalOpen(false), [])
31+
32+
const value = useMemo(
33+
() => ({
34+
selectedPayout,
35+
setSelectedPayout,
36+
isInvoiceModalOpen,
37+
openInvoiceModal,
38+
closeInvoiceModal,
39+
}),
40+
[
41+
selectedPayout,
42+
setSelectedPayout,
43+
isInvoiceModalOpen,
44+
openInvoiceModal,
45+
closeInvoiceModal,
46+
],
47+
)
2448

2549
return (
26-
<PayoutContext.Provider
27-
value={{
28-
selectedPayout,
29-
setSelectedPayout,
30-
isInvoiceModalOpen,
31-
openInvoiceModal,
32-
closeInvoiceModal,
33-
}}
34-
>
35-
{children}
36-
</PayoutContext.Provider>
50+
<PayoutContext.Provider value={value}>{children}</PayoutContext.Provider>
3751
)
3852
}
3953

clients/apps/web/src/components/Products/Benefits/BenefitSearchComplex.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export const BenefitSearchComplex = ({
4646
}, [searchQuery])
4747

4848
useEffect(() => {
49+
// eslint-disable-next-line react-hooks/no-deriving-state-in-effects -- isDropdownOpen is also set by click-outside and focus handlers
4950
setIsDropdownOpen(debouncedQuery.trim().length > 0)
5051
}, [debouncedQuery])
5152

clients/apps/web/src/components/SyntaxHighlighterShiki/SyntaxHighlighterClient.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
'use client'
22

3-
import React, { useCallback, useContext, useEffect, useState } from 'react'
3+
import React, {
4+
useCallback,
5+
useContext,
6+
useEffect,
7+
useMemo,
8+
useState,
9+
} from 'react'
410
import { BundledLanguage } from 'shiki'
511
import { createHighlighterCore, HighlighterCore } from 'shiki/core'
612
import { createOnigurumaEngine } from 'shiki/engine/oniguruma'
@@ -94,14 +100,17 @@ export const SyntaxHighlighterProvider = ({
94100
[highlighter],
95101
)
96102

103+
const value = useMemo(
104+
() => ({
105+
highlighter,
106+
loadedLanguages,
107+
loadLanguage: _loadLanguage,
108+
}),
109+
[highlighter, loadedLanguages, _loadLanguage],
110+
)
111+
97112
return (
98-
<SyntaxHighlighterContext.Provider
99-
value={{
100-
highlighter,
101-
loadedLanguages,
102-
loadLanguage: _loadLanguage,
103-
}}
104-
>
113+
<SyntaxHighlighterContext.Provider value={value}>
105114
{children}
106115
</SyntaxHighlighterContext.Provider>
107116
)

clients/apps/web/src/hooks/queries/customerPortal.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { Client, operations, schemas, unwrap } from '@polar-sh/client'
55
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
66
import { defaultRetry } from './retry'
77

8+
/* eslint-disable @tanstack/query/exhaustive-deps -- api and client are stable module-level singletons */
9+
810
export function useCustomerPortalCustomer(options?: {
911
initialData?: schemas['CustomerPortalCustomer']
1012
}) {

clients/apps/web/src/hooks/queries/files.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ export const useFiles = (
1515
options?: { limit?: number },
1616
) =>
1717
useQuery({
18-
queryKey: ['user', 'files', JSON.stringify(fileIds)],
18+
queryKey: [
19+
'user',
20+
'files',
21+
JSON.stringify(fileIds),
22+
organizationId,
23+
options?.limit,
24+
],
1925
queryFn: () =>
2026
unwrap(
2127
api.GET('/v1/files/', {

0 commit comments

Comments
 (0)