Skip to content

Commit 00a5c96

Browse files
authored
Add react-query lint rules (#1544)
Helps avoid foot guns / re-rendering issues / bad habits with react-query Fixes the existing lint errors/warnings we had with react-query or makes notes on them
1 parent 7ec48ee commit 00a5c96

32 files changed

+292
-280
lines changed

App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ LogBox.ignoreLogs([
5555
'event="noNetwork', // ethers
5656
"[Reanimated] Reading from `value` during component render. Please ensure that you do not access the `value` property or use `get` method of a shared value while React is rendering a component.",
5757
"Attempted to import the module",
58+
"Falling back to file-based resolution. Consider updating the call site or asking the package maintainer(s) to expose this API",
5859
"Couldn't find real values for `KeyboardContext`. Please make sure you're inside of `KeyboardProvider` - otherwise functionality of `react-native-keyboard-controller` will not work. [Component Stack]",
5960
"sync worker error storage error: Pool needs to reconnect before use",
6061
"[Converse.debug.dylib] sync worker error storage error: Pool needs to reconnect before use",

eslint.config.mjs

Lines changed: 80 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -4,141 +4,98 @@ import pluginReactHooks from "eslint-plugin-react-hooks";
44
import pluginReactNative from "eslint-plugin-react-native";
55
import tsESLint from "typescript-eslint";
66
import customPlugin from "./custom-eslint-plugin/index.js";
7+
import pluginQuery from "@tanstack/eslint-plugin-query";
78

89
// While eslint-plugin-react-native fix to handle eslint flat config (https://github.com/Intellicode/eslint-plugin-react-native/issues/333#issuecomment-2150582430)
910
import { fixupPluginRules } from "@eslint/compat";
1011

11-
/** @type {import('eslint').Linter.Config[]} */
12-
export default [
13-
{
14-
files: ["**/*.{ts,tsx}"],
15-
ignores: [],
16-
languageOptions: {
17-
parser: tsESLint.parser,
18-
},
19-
plugins: {
20-
react: pluginReact,
21-
"typescript-eslint": tsESLint.plugin,
22-
import: pluginImport,
23-
"react-hooks": pluginReactHooks,
24-
"react-native": fixupPluginRules(pluginReactNative),
25-
"custom-plugin": customPlugin,
26-
},
27-
rules: {
28-
...tsESLint.configs["recommended"].rules,
29-
...pluginReact.configs["recommended"].rules,
30-
...pluginImport.configs["recommended"].rules,
31-
...pluginReactHooks.configs["recommended"].rules,
32-
...pluginReactNative.configs["all"].rules,
33-
34-
"import/no-unresolved": "off", // Disable since TypeScript already handles module resolution and import validation
35-
"import/no-relative-parent-imports": "off", // Disable for now
36-
"import/no-default-export": "warn",
37-
"import/order": "off", // Since we use @design-system/, etc... there seems to be a lot of false positives
12+
/** @type {import('eslint').Linter.Config} */
13+
const config = {
14+
files: ["**/*.{ts,tsx}"],
15+
ignores: [],
16+
languageOptions: {
17+
parser: tsESLint.parser,
18+
},
19+
plugins: {
20+
"@tanstack/query": pluginQuery,
21+
react: pluginReact,
22+
"typescript-eslint": tsESLint.plugin,
23+
import: pluginImport,
24+
"react-hooks": pluginReactHooks,
25+
"react-native": fixupPluginRules(pluginReactNative),
26+
"custom-plugin": customPlugin,
27+
},
28+
rules: {
29+
...tsESLint.configs["recommended"].rules,
30+
...pluginReact.configs["recommended"].rules,
31+
...pluginImport.configs["recommended"].rules,
32+
...pluginReactHooks.configs["recommended"].rules,
33+
...pluginReactNative.configs["all"].rules,
34+
...pluginQuery.configs["recommended"].rules,
3835

39-
"no-restricted-imports": [
40-
"error",
41-
{
42-
paths: [
43-
{
44-
name: "i18n-js",
45-
message: "Use @i18n app module instead.",
46-
},
47-
],
48-
},
49-
],
50-
"no-restricted-imports": [
51-
"warn",
52-
{
53-
patterns: [
54-
{
55-
group: ["../../*", "../../../*"],
56-
message:
57-
"Please use module aliases (e.g., @design-system/) instead of relative paths when going up more than one directory",
58-
},
59-
],
60-
},
61-
],
36+
"@tanstack/query/exhaustive-deps": "error",
37+
"@tanstack/query/no-rest-destructuring": "warn",
38+
"@tanstack/query/stable-query-client": "error",
39+
"@tanstack/query/no-unstable-deps": "warn",
6240

63-
"react-hooks/exhaustive-deps": "error",
64-
"react-hooks/rules-of-hooks": "error",
41+
"import/no-unresolved": "off",
42+
"import/no-relative-parent-imports": "off",
43+
"import/no-default-export": "warn",
44+
"import/order": "off",
6545

66-
"react-native/no-raw-text": "off", // We have so many Text wrapper components... and eslint doesn't know that OnboardingTitleSubtitle.Title is a Text component
67-
"react-native/no-color-literals": "warn", // We need to use the colors in theme!
68-
"react-native/sort-styles": "off", // Not needed
69-
"react-native/no-unused-styles": "off", // Because it's giving warning for styles we use in useStyles hook even though they are used
70-
"react-native/no-inline-styles": "warn",
46+
"no-restricted-imports": [
47+
"error",
48+
{
49+
paths: [
50+
{
51+
name: "i18n-js",
52+
message: "Use @i18n app module instead.",
53+
},
54+
],
55+
},
56+
],
7157

72-
"react/no-unescaped-entities": "off", // Not needed
73-
"react/prop-types": "off", // Disable since we use TypeScript
74-
"react/display-name": "off", // Not needed
75-
"react/react-in-jsx-scope": "off", // Disable since we use React 18
76-
"react/jsx-key": "error",
77-
"react/jsx-no-bind": [
78-
"off", // Until we finish the HUGEEEE refactor
79-
{
80-
ignoreRefs: true,
81-
allowArrowFunctions: false,
82-
allowFunctions: false,
83-
allowBind: false,
84-
ignoreDOMComponents: true,
85-
},
86-
],
58+
"react-hooks/exhaustive-deps": "error",
59+
"react-hooks/rules-of-hooks": "error",
8760

88-
"typescript-eslint/no-unused-vars": [
89-
"warn",
90-
{
91-
vars: "all",
92-
args: "none",
93-
ignoreRestSiblings: true,
94-
},
95-
],
96-
"typescript-eslint/no-explicit-any": "warn",
97-
"typescript-eslint/explicit-function-return-type": "off",
98-
"typescript-eslint/consistent-type-definitions": ["warn", "type"],
61+
"react-native/no-raw-text": "off",
62+
"react-native/no-color-literals": "warn",
63+
"react-native/sort-styles": "off",
64+
"react-native/no-unused-styles": "off",
65+
"react-native/no-inline-styles": "warn",
9966

100-
"padding-line-between-statements": [
101-
"off", // Off for now until we all agree on best practices
102-
{
103-
blankLine: "always",
104-
prev: "multiline-const",
105-
next: "multiline-expression",
106-
},
107-
{
108-
blankLine: "always",
109-
prev: "multiline-expression",
110-
next: "multiline-const",
111-
},
112-
{ blankLine: "always", prev: "multiline-block-like", next: "*" },
113-
{ blankLine: "always", prev: "*", next: "multiline-block-like" },
114-
],
67+
"react/no-unescaped-entities": "off",
68+
"react/prop-types": "off",
69+
"react/display-name": "off",
70+
"react/react-in-jsx-scope": "off",
71+
"react/jsx-key": "error",
72+
"react/jsx-no-bind": "off",
11573

116-
"prettier/prettier": "off", // We use prettier manually with linted-staged for example
74+
"typescript-eslint/no-unused-vars": [
75+
"warn",
76+
{
77+
vars: "all",
78+
args: "none",
79+
ignoreRestSiblings: true,
80+
},
81+
],
82+
"typescript-eslint/no-explicit-any": "warn",
83+
"typescript-eslint/explicit-function-return-type": "off",
84+
"typescript-eslint/consistent-type-definitions": ["warn", "type"],
11785

118-
"custom-plugin/padding-before-react-hooks": "warn",
86+
"padding-line-between-statements": "off",
87+
"prettier/prettier": "off",
88+
"custom-plugin/padding-before-react-hooks": "warn",
89+
},
90+
settings: {
91+
"import/resolver": {
92+
typescript: true,
93+
node: true,
11994
},
120-
settings: {
121-
"import/resolver": {
122-
typescript: true,
123-
node: true,
124-
},
125-
react: {
126-
version: "detect",
127-
},
95+
react: {
96+
version: "detect",
12897
},
12998
},
130-
// Custom logics to enable single function per file for files in utils directory so we can more easily implement tests for them (maybe)
131-
// {
132-
// name: "custom/utils-typescript",
133-
// files: ["utils/**/*.ts"],
134-
// languageOptions: {
135-
// parser: parserTypescript,
136-
// },
137-
// plugins: {
138-
// "custom-plugin": customPlugin,
139-
// },
140-
// rules: {
141-
// "custom-plugin/single-function-per-file": "off", // Off for now
142-
// },
143-
// },
144-
];
99+
};
100+
101+
export default config;

features/conversation-list/use-conversation-list-conversations.tsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import { useEffect, useMemo } from "react";
1212
export const useConversationListConversations = () => {
1313
const currentAccount = useCurrentAccount();
1414

15-
const { data: conversations, ...rest } = useQuery(
15+
const {
16+
data: conversations,
17+
isLoading,
18+
refetch,
19+
} = useQuery(
1620
getConversationsQueryOptions({
1721
account: currentAccount!,
1822
})
@@ -32,13 +36,13 @@ export const useConversationListConversations = () => {
3236

3337
// For now, let's make sure we always are up to date with the conversations
3438
useScreenFocusEffectOnce(() => {
35-
rest.refetch().catch(captureError);
39+
refetch().catch(captureError);
3640
});
3741

3842
// For now, let's make sure we always are up to date with the conversations
3943
useAppStateHandlers({
4044
onForeground: () => {
41-
rest.refetch().catch(captureError);
45+
refetch().catch(captureError);
4246
},
4347
});
4448

@@ -49,6 +53,16 @@ export const useConversationListConversations = () => {
4953
topic: conversation.topic,
5054
})
5155
),
56+
// note/todo(lustig): investigate combine to remove need for filteredConvresations (which utilizes conversationsDataQueries)
57+
// which is referentially unstable
58+
// combine: (queries) => {
59+
// queries.forEach((query) => {
60+
// console.log("query", query);
61+
// if (query) {
62+
// isConversationAllowed(query.data);
63+
// }
64+
// });
65+
// },
5266
});
5367

5468
const filteredAndSortedConversations = useMemo(() => {
@@ -71,7 +85,16 @@ export const useConversationListConversations = () => {
7185
const timestampB = b.lastMessage?.sentNs ?? 0;
7286
return timestampB - timestampA;
7387
});
88+
/*
89+
* note(lustig): potential fix using existing libraries could be exploring `combine` above
90+
* es lint from @tanstack/query/no-unstable-deps
91+
*
92+
* lint error: The result of useQueries is not referentially stable, so don't pass it
93+
* directly into the dependencies array of useMemo. Instead, destructure
94+
* the return value of useQueries and pass the destructured values into
95+
* the dependency array of useMemo.eslint@tanstack/query/no-unstable-deps
96+
*/
7497
}, [conversations, conversationsMetadataQueries]);
7598

76-
return { data: filteredAndSortedConversations, ...rest };
99+
return { data: filteredAndSortedConversations, isLoading, refetch };
77100
};

features/conversation/conversation-message/use-conversation-message.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ export function useConversationMessageById({
1919

2020
const cachedMessage = messages?.byId[messageId];
2121

22-
// Only fetch the message if it's not already in the conversation messages
2322
const { data: message, isLoading: isLoadingMessage } = useQuery({
2423
...getConversationMessageQueryOptions({
2524
account: currentAccount,
2625
messageId,
2726
}),
27+
// Only fetch the message if it's not already in the conversation messages
2828
enabled: !cachedMessage && !!messageId && !!currentAccount,
2929
});
3030

features/conversation/conversation-preview/conversation-preview.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import { ConversationMessageReactions } from "@/features/conversation/conversati
1010
import { ConversationMessageTimestamp } from "@/features/conversation/conversation-message/conversation-message-timestamp";
1111
import { MessageContextStoreProvider } from "@/features/conversation/conversation-message/conversation-message.store-context";
1212
import { conversationListDefaultProps } from "@/features/conversation/conversation-messages-list";
13-
import { useConversationPreviewMessages } from "@/features/conversation/conversation-preview/conversation-preview-messages.query";
1413
import { ConversationStoreProvider } from "@/features/conversation/conversation.store-context";
1514
import { useConversationQuery } from "@/queries/useConversationQuery";
1615
import { $globalStyles } from "@/theme/styles";
1716
import type { ConversationTopic } from "@xmtp/react-native-sdk";
1817
import React from "react";
1918
import { FlatList } from "react-native";
19+
import { useConversationPreviewMessages } from "./conversation-preview-messages.query";
2020

2121
type ConversationPreviewProps = {
2222
topic: ConversationTopic;
@@ -26,7 +26,7 @@ export const ConversationPreview = ({ topic }: ConversationPreviewProps) => {
2626
const currentAccount = useCurrentAccount()!;
2727

2828
const { data: messages, isLoading: isLoadingMessages } =
29-
useConversationPreviewMessages(currentAccount, topic!);
29+
useConversationPreviewMessages(currentAccount, topic);
3030

3131
const { data: conversation, isLoading: isLoadingConversation } =
3232
useConversationQuery({

features/conversation/conversation-screen-header/conversation-screen-group-header-title.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const GroupConversationTitle = memo(
3535

3636
const { data: members } = useGroupMembersQuery({
3737
account: currentAccount,
38-
topic: topic!,
38+
topic,
3939
});
4040

4141
const { data: memberData } = useGroupMembersAvatarData({ topic });

features/profiles/profile.nav.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { NativeStack } from "@/screens/Navigation/Navigation";
2+
import { ProfileScreen } from "./profile.screen";
3+
import { ConversationTopic } from "@xmtp/react-native-sdk";
4+
5+
export type ProfileNavParams = {
6+
address: string;
7+
fromGroupTopic?: ConversationTopic;
8+
};
9+
10+
export const ProfileScreenConfig = {
11+
path: "/profile",
12+
};
13+
14+
export function ProfileNav() {
15+
return <NativeStack.Screen name="Profile" component={ProfileScreen} />;
16+
}

screens/Profile.tsx renamed to features/profiles/profile.screen.tsx

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
import React, { useCallback, useState } from "react";
2-
import {
3-
View,
4-
ViewStyle,
5-
Alert,
6-
Share,
7-
Platform,
8-
Linking,
9-
TextStyle,
10-
} from "react-native";
2+
import { View, ViewStyle, Alert, Share } from "react-native";
113
import { Screen } from "@/components/Screen/ScreenComp/Screen";
124
import { ContactCard } from "@/features/profiles/components/contact-card";
135
import { SettingsList } from "@/design-system/settings-list/settings-list";
@@ -42,7 +34,7 @@ import { iconRegistry } from "@/design-system/Icon/Icon";
4234
import { useNotificationsPermission } from "@/features/notifications/hooks/use-notifications-permission";
4335
import { SocialNames } from "@/features/profiles/components/social-names";
4436

45-
export default function ProfileScreen() {
37+
export function ProfileScreen() {
4638
const [editMode, setEditMode] = useState(false);
4739
const { theme, themed } = useAppTheme();
4840
const router = useRouter();
@@ -52,11 +44,8 @@ export default function ProfileScreen() {
5244
const isMyProfile = peerAddress.toLowerCase() === userAddress?.toLowerCase();
5345
const setPeersStatus = useSettingsStore((s) => s.setPeersStatus);
5446
const { data: socials } = useProfileSocials(peerAddress);
55-
const {
56-
notificationsPermissionStatus,
57-
requestPermission,
58-
setNotificationsSettings,
59-
} = useNotificationsPermission();
47+
const { notificationsPermissionStatus, requestPermission } =
48+
useNotificationsPermission();
6049

6150
const userName = usePreferredUsername(peerAddress);
6251
const displayName = usePreferredName(peerAddress);

0 commit comments

Comments
 (0)