Skip to content

Commit ee582a0

Browse files
authored
Merge pull request #5001 from kobotoolbox/react-query-start
Set up React Query to simplify state management
2 parents 9608bd4 + da7576f commit ee582a0

File tree

7 files changed

+336
-84
lines changed

7 files changed

+336
-84
lines changed

.eslintrc.js

Lines changed: 133 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,23 @@
11
const jsRules = {
22
'no-empty-pattern': 1,
33
'arrow-body-style': [1, 'as-needed', {requireReturnForObjectLiteral: true}],
4-
'arrow-parens': [1, 'always'],
5-
'arrow-spacing': 1,
6-
'block-spacing': [1, 'never'],
7-
'brace-style': [1, '1tbs', {allowSingleLine: true}],
8-
'comma-dangle': [1, 'always-multiline'],
9-
'comma-spacing': [1, {before: false, after: true}],
104
curly: 1,
11-
'eol-last': [1, 'always'],
125
eqeqeq: 1,
13-
'func-call-spacing': 1,
14-
'jsx-quotes': ['warn', 'prefer-single'],
15-
'key-spacing': [1, {beforeColon: false, afterColon: true, mode: 'strict'}],
166
'max-nested-callbacks': [1, 6],
177
'no-case-declarations': 1,
18-
'no-confusing-arrow': 1,
198
'no-console': [1, {allow: ['info', 'warn', 'error']}],
209
'no-control-regex': 0,
2110
'no-debugger': 1,
2211
'no-duplicate-imports': 1,
2312
'no-extend-native': 1,
2413
'no-extra-bind': 1,
2514
'no-extra-boolean-cast': 1,
26-
'no-extra-semi': 1,
2715
'no-fallthrough': 1,
2816
'no-inner-declarations': 1,
2917
'no-irregular-whitespace': 1,
3018
'no-lonely-if': 1,
31-
'no-mixed-spaces-and-tabs': 1,
32-
'no-multi-spaces': 1,
33-
'no-multiple-empty-lines': [1, {max: 2, maxEOF: 1}],
3419
'no-nonoctal-decimal-escape': 1,
3520
'no-prototype-builtins': 1,
36-
'no-trailing-spaces': 1,
3721
'no-undef-init': 1,
3822
'no-undef': 1,
3923
'no-unexpected-multiline': 1,
@@ -44,12 +28,10 @@ const jsRules = {
4428
'no-useless-escape': 1,
4529
'no-useless-rename': 1,
4630
'no-var': 1,
47-
'no-whitespace-before-property': 1,
4831
'one-var': ['warn', 'never'],
4932
'prefer-const': 1,
5033
'prefer-rest-params': 1,
5134
'prefer-spread': 1,
52-
quotes: ['warn', 'single', {avoidEscape: true}],
5335
'react/jsx-boolean-value': 1,
5436
'react/jsx-no-undef': 2,
5537
'react/jsx-sort-prop-types': 0,
@@ -64,13 +46,87 @@ const jsRules = {
6446
'react/react-in-jsx-scope': 2,
6547
'react/self-closing-comp': 2,
6648
'react/wrap-multilines': 0,
49+
strict: 1,
50+
51+
// Formatting (stylistic) rules.
52+
//
53+
// 'Deprecated' in ESLint 8.53.0, see announcement
54+
// - Announcement: https://eslint.org/blog/2023/10/deprecating-formatting-rules/
55+
// - Rules deprecated: (https://github.com/eslint/eslint/commit/528e1c00dc2a)
56+
//
57+
// Successor: ESLint Stylistic, community project
58+
// - Rules: https://eslint.style/packages/default#rules
59+
//
60+
// It'll be useful to keep a variant of these rules around as an alternative
61+
// "auto-fixer" (versus Prettier), since Prettier modifies line wrappings in
62+
// a way that can harm code readability at times.
63+
//
64+
// Idea:
65+
// * Create a 'stylistic-only' ESLint config with these rules
66+
// * Use the stylistic config for 'auto-fix' (lightweight formatting) in editor
67+
// (normalize quotes, commas, semicolons, etc. without touching linebreaks)
68+
// * Remove 'stylistic' rules from the main config, to reduce diagnostic
69+
// clutter in code or in the 'Problems' pane
70+
// * Keep an optional well-configured 'Prettier' tooling, able to run on
71+
// modified lines or current/selected paragraph, around for convenience
72+
'arrow-parens': [1, 'always'],
73+
'arrow-spacing': 1,
74+
'block-spacing': [1, 'never'],
75+
'brace-style': [1, '1tbs', {allowSingleLine: true}],
76+
'comma-dangle': [1, 'always-multiline'],
77+
'comma-spacing': [1, {before: false, after: true}],
78+
'eol-last': [1, 'always'],
79+
'func-call-spacing': 1,
80+
'jsx-quotes': ['warn', 'prefer-single'],
81+
'key-spacing': [1, {beforeColon: false, afterColon: true, mode: 'strict'}],
82+
'no-confusing-arrow': 1,
83+
'no-extra-semi': 1,
84+
'no-mixed-spaces-and-tabs': 1,
85+
'no-multi-spaces': 1, // Comment out to permit columnar formatting
86+
'no-multiple-empty-lines': [1, {max: 2, maxEOF: 1}],
87+
'no-trailing-spaces': 1,
88+
'no-whitespace-before-property': 1,
89+
'quotes': ['warn', 'single', {avoidEscape: true}],
6790
'semi-spacing': [1, {before: false, after: true}],
6891
'semi-style': [1, 'last'],
69-
semi: 1,
92+
'semi': 1,
7093
'space-before-function-paren': 'off',
7194
'space-in-parens': [1, 'never'],
7295
'space-infix-ops': 1,
73-
strict: 1,
96+
97+
// React Plugin - Default to plugin:react/recommended.
98+
// https://github.com/jsx-eslint/eslint-plugin-react
99+
100+
// TODO: Set these rules back to 'error' severity and fix the affected code
101+
// (1) comment these out (2) run npx eslint (...) with --quiet to list only errors
102+
// ===============================================================================
103+
// Deprecated API
104+
'react/no-deprecated': 1, // e.g. componentWillReceiveProps
105+
'react/no-find-dom-node': 1, // Do not use findDOMNode
106+
'react/no-string-refs': 1, // Using string literals in ref attributes is deprecated
107+
// Misuse
108+
'react/jsx-key': 1, // Missing "key" prop for element in iterator
109+
'react/no-direct-mutation-state': 1, // Do not mutate state directly. Use setState()
110+
// Other
111+
'react/display-name': 1, // "Component definition is missing display name"
112+
'react/no-unescaped-entities': 1, // e.g. `"` can be escaped with `"`…
113+
114+
// OK
115+
// ==
116+
'react/jsx-no-target-blank': 0, // Using target="_blank" without rel="noreferrer"
117+
// …not a problem for modern browsers; see 2021 update
118+
// (https://mathiasbynens.github.io/rel-noopener/#recommendations)
119+
120+
121+
// React Hooks - Default to plugin:react-hooks/recommended
122+
// https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks
123+
124+
// TODO: Fix these! They should be errors, too.
125+
'react-hooks/rules-of-hooks': 1, // TODO: make this an error, not warning
126+
127+
// React Query - Default to plugin:@tanstack/eslint-plugin-query/recommended
128+
// https://tanstack.com/query/latest/docs/eslint/eslint-plugin-query
129+
74130
};
75131

76132
// TypeScript rules override some of JavaScript rules plus add a few more.
@@ -79,19 +135,8 @@ const tsRules = Object.assign({}, jsRules, {
79135
// Would be good to enable in future, when most of codebase is TS.
80136
'@typescript-eslint/ban-types': 'off',
81137
'@typescript-eslint/ban-ts-comment': 1,
82-
'@typescript-eslint/comma-dangle': [1, 'always-multiline'],
83-
'@typescript-eslint/comma-spacing': [1, {before: false, after: true}],
84138
'@typescript-eslint/consistent-type-definitions': [1, 'interface'],
85139
'@typescript-eslint/consistent-type-imports': [1, {prefer: 'type-imports'}],
86-
'@typescript-eslint/func-call-spacing': [1, 'never'],
87-
'@typescript-eslint/keyword-spacing': [1, {before: true, after: true}],
88-
'@typescript-eslint/member-delimiter-style': [
89-
1,
90-
{
91-
multiline: {delimiter: 'semi'},
92-
singleline: {delimiter: 'semi'},
93-
},
94-
],
95140
'@typescript-eslint/method-signature-style': [1, 'property'],
96141
'@typescript-eslint/naming-convention': [
97142
1,
@@ -127,8 +172,6 @@ const tsRules = Object.assign({}, jsRules, {
127172
'@typescript-eslint/no-empty-function': 1,
128173
'@typescript-eslint/no-empty-interface': 1,
129174
'@typescript-eslint/no-explicit-any': 1,
130-
'@typescript-eslint/no-extra-parens': 'off',
131-
'@typescript-eslint/no-extra-semi': 1,
132175
'@typescript-eslint/no-inferrable-types': 1,
133176
'@typescript-eslint/no-invalid-void-type': 1,
134177
'@typescript-eslint/no-loop-func': 1,
@@ -139,38 +182,11 @@ const tsRules = Object.assign({}, jsRules, {
139182
'@typescript-eslint/no-use-before-define': 1,
140183
'@typescript-eslint/no-useless-constructor': 1,
141184
'@typescript-eslint/no-var-requires': 'off',
142-
'@typescript-eslint/object-curly-spacing': 1,
143185
'@typescript-eslint/parameter-properties': 1,
144186
'@typescript-eslint/prefer-enum-initializers': 1,
145187
'@typescript-eslint/prefer-optional-chain': 1,
146-
'@typescript-eslint/quotes': ['warn', 'single', {avoidEscape: true}],
147-
'@typescript-eslint/semi': 1,
148188
'@typescript-eslint/sort-type-union-intersection-members': 'off',
149-
'@typescript-eslint/space-before-function-paren': [
150-
1,
151-
{
152-
anonymous: 'always',
153-
named: 'never',
154-
asyncArrow: 'always', // TODO: Defer all formatting rules to Prettier
155-
},
156-
],
157-
'@typescript-eslint/type-annotation-spacing': [
158-
1,
159-
{
160-
before: false,
161-
after: true,
162-
overrides: {
163-
arrow: {
164-
before: true,
165-
after: true,
166-
},
167-
},
168-
},
169-
],
170189
'@typescript-eslint/unified-signatures': 1,
171-
'comma-dangle': 'off',
172-
'comma-spacing': 'off',
173-
'func-call-spacing': 'off',
174190
// The 'import' plugin supports separately importing types
175191
// (@typescript-eslint/no-duplicate-imports is deprecated)
176192
'import/no-duplicates': 1,
@@ -185,8 +201,47 @@ const tsRules = Object.assign({}, jsRules, {
185201
'no-unused-vars': 'off',
186202
'no-useless-backreference': 'off',
187203
'no-var': 1,
188-
quotes: 'off',
189-
semi: 'off',
204+
205+
// Formatting (stylistic) rules, for TypeScript.
206+
// 'Deprecated' in @typescript-eslint v6.16.0 (https://typescript-eslint.io/blog/deprecating-formatting-rules/),
207+
// Still useful for us in some form, particularly with auto-fix.
208+
// See "Formatting (stylistic) rules" comment, above.
209+
'@typescript-eslint/comma-dangle': [1, 'always-multiline'],
210+
'@typescript-eslint/comma-spacing': [1, {before: false, after: true}],
211+
'@typescript-eslint/func-call-spacing': [1, 'never'],
212+
'@typescript-eslint/keyword-spacing': [1, {before: true, after: true}],
213+
'@typescript-eslint/no-extra-parens': 'off',
214+
'@typescript-eslint/no-extra-semi': 1,
215+
'@typescript-eslint/object-curly-spacing': 1,
216+
'@typescript-eslint/member-delimiter-style': [1, {
217+
multiline: {delimiter: 'semi'},
218+
singleline: {delimiter: 'semi'},
219+
}],
220+
'@typescript-eslint/quotes': ['warn', 'single', {avoidEscape: true}],
221+
'@typescript-eslint/semi': 1,
222+
'@typescript-eslint/type-annotation-spacing': [1, {
223+
before: false,
224+
after: true,
225+
overrides: {
226+
arrow: {
227+
before: true,
228+
after: true,
229+
},
230+
},
231+
}],
232+
'@typescript-eslint/space-before-function-paren': [1, {
233+
anonymous: 'always',
234+
named: 'never',
235+
asyncArrow: 'always',
236+
}],
237+
// Turn off equivalent ESLint rules
238+
'comma-dangle': 'off',
239+
'comma-spacing': 'off',
240+
'func-call-spacing': 'off',
241+
'no-extra-semi': 'off',
242+
'quotes': 'off',
243+
'semi': 'off',
244+
190245
});
191246

192247
module.exports = {
@@ -206,12 +261,27 @@ module.exports = {
206261
},
207262
ignorePatterns: ['**/*.scss'],
208263
plugins: ['react'],
209-
extends: ['eslint:recommended', 'prettier', 'plugin:storybook/recommended'],
264+
extends: [
265+
'eslint:recommended',
266+
'plugin:react/recommended', // Use recommended rules: https://github.com/jsx-eslint/eslint-plugin-react#list-of-supported-rules
267+
'plugin:react/jsx-runtime', // Use the new JSX transform
268+
'plugin:react-hooks/recommended', // Rules of Hooks (https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks)
269+
'plugin:@tanstack/eslint-plugin-query/recommended', // For Tanstack Query (aka react-query)
270+
'prettier',
271+
'plugin:storybook/recommended'],
210272
rules: jsRules,
211273
settings: {
212274
react: {
213275
version: 'detect',
214276
},
277+
// https://github.com/jsx-eslint/eslint-plugin-react#configuration-legacy-eslintrc-
278+
// Try to include all the component wrapper functions we use here
279+
componentWrapperFunctions: [
280+
'observer', // MobX observer
281+
],
282+
linkComponenets: [
283+
{ name: 'Link', linkAttribute: 'to' }, // React Router Link
284+
],
215285
},
216286
overrides: [
217287
{

jsapp/js/alertify.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {KeyNames} from 'js/constants';
44
import type {IconName} from 'jsapp/fonts/k-icons';
55
import {escapeHtml} from 'js/utils';
66
import type {ReactElement} from 'react';
7-
import {render} from 'react-dom';
7+
import {createRoot} from 'react-dom/client';
8+
89

910
interface MultiConfirmButton {
1011
label: string;
@@ -193,6 +194,9 @@ export function destroyConfirm(
193194
*/
194195
export function renderJSXMessage(jsx: ReactElement) {
195196
const domNode = document.createElement('div');
196-
render(jsx, domNode);
197-
return domNode.outerHTML;
197+
const root = createRoot(domNode);
198+
root.render(jsx);
199+
const str = domNode.outerHTML;
200+
root.unmount();
201+
return str;
198202
}

jsapp/js/app.jsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ import {
2929
import {isAnyProcessingRouteActive} from 'js/components/processing/routes.utils';
3030
import pageState from 'js/pageState.store';
3131

32+
// Query-related
33+
import { QueryClientProvider } from '@tanstack/react-query'
34+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
35+
import { queryClient } from './query/queryClient.ts'
36+
3237
class App extends React.Component {
3338
constructor(props) {
3439
super(props);
@@ -100,7 +105,7 @@ class App extends React.Component {
100105
// opt for a more sane, and singluar(!) solution.
101106
return (
102107
<DocumentTitle title='KoboToolbox'>
103-
<>
108+
<QueryClientProvider client={queryClient}>
104109
<RootContextProvider>
105110
<Tracking />
106111
<ToasterConfig />
@@ -139,7 +144,23 @@ class App extends React.Component {
139144
</bem.PageWrapper__content>
140145
</bem.PageWrapper>
141146
</RootContextProvider>
142-
</>
147+
148+
149+
{/* React Query Devtools - GUI for inspecting and modifying query status
150+
(https://tanstack.com/query/latest/docs/framework/react/devtools)
151+
They only show up in dev server (NODE_ENV==='development')
152+
Additionally, we're keeping them commented out in `beta`
153+
(https://github.com/kobotoolbox/kpi/pull/5001#discussion_r1691067344)
154+
(1) Uncomment if you want to use these tools
155+
(2) The <style> tag lowers the toggle button opacity
156+
to make it less prominent in dev screenshots. */}
157+
{/*
158+
<style>{'.tsqd-open-btn-container { opacity: 0.1 !important; };'}</style>
159+
<ReactQueryDevtools />
160+
*/}
161+
162+
163+
</QueryClientProvider>
143164
</DocumentTitle>
144165
);
145166
}

jsapp/js/formbuild/renderInBackbone.es6

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,8 @@ export function renderKobomatrix (view, el) {
4747
let model = new KoboMatrixRow(view.model);
4848
const root = createRoot(el.get(0));
4949
root.render(<KoboMatrix model={model} />);
50+
// TODO: should this root be unmounted at some point?
51+
// https://react.dev/reference/react-dom/client/createRoot#root-unmount
52+
// Maybe instantiate the root in KoboMatrixView, then unmount it when
53+
// KoboMatrixView is disposed.
5054
}

jsapp/js/query/queryClient.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { QueryClient } from '@tanstack/react-query'
2+
3+
// Some shared defaults and config can be set here!
4+
// Docs: https://tanstack.com/query/v5/docs/reference/QueryClient#queryclient
5+
// See: https://tanstack.com/query/v5/docs/framework/react/guides/important-defaults
6+
const queryClient = new QueryClient()
7+
8+
export { queryClient }

0 commit comments

Comments
 (0)