Skip to content

Commit

Permalink
Merge pull request #5001 from kobotoolbox/react-query-start
Browse files Browse the repository at this point in the history
Set up React Query to simplify state management
  • Loading branch information
p2edwards authored Aug 12, 2024
2 parents 9608bd4 + da7576f commit ee582a0
Show file tree
Hide file tree
Showing 7 changed files with 336 additions and 84 deletions.
196 changes: 133 additions & 63 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,23 @@
const jsRules = {
'no-empty-pattern': 1,
'arrow-body-style': [1, 'as-needed', {requireReturnForObjectLiteral: true}],
'arrow-parens': [1, 'always'],
'arrow-spacing': 1,
'block-spacing': [1, 'never'],
'brace-style': [1, '1tbs', {allowSingleLine: true}],
'comma-dangle': [1, 'always-multiline'],
'comma-spacing': [1, {before: false, after: true}],
curly: 1,
'eol-last': [1, 'always'],
eqeqeq: 1,
'func-call-spacing': 1,
'jsx-quotes': ['warn', 'prefer-single'],
'key-spacing': [1, {beforeColon: false, afterColon: true, mode: 'strict'}],
'max-nested-callbacks': [1, 6],
'no-case-declarations': 1,
'no-confusing-arrow': 1,
'no-console': [1, {allow: ['info', 'warn', 'error']}],
'no-control-regex': 0,
'no-debugger': 1,
'no-duplicate-imports': 1,
'no-extend-native': 1,
'no-extra-bind': 1,
'no-extra-boolean-cast': 1,
'no-extra-semi': 1,
'no-fallthrough': 1,
'no-inner-declarations': 1,
'no-irregular-whitespace': 1,
'no-lonely-if': 1,
'no-mixed-spaces-and-tabs': 1,
'no-multi-spaces': 1,
'no-multiple-empty-lines': [1, {max: 2, maxEOF: 1}],
'no-nonoctal-decimal-escape': 1,
'no-prototype-builtins': 1,
'no-trailing-spaces': 1,
'no-undef-init': 1,
'no-undef': 1,
'no-unexpected-multiline': 1,
Expand All @@ -44,12 +28,10 @@ const jsRules = {
'no-useless-escape': 1,
'no-useless-rename': 1,
'no-var': 1,
'no-whitespace-before-property': 1,
'one-var': ['warn', 'never'],
'prefer-const': 1,
'prefer-rest-params': 1,
'prefer-spread': 1,
quotes: ['warn', 'single', {avoidEscape: true}],
'react/jsx-boolean-value': 1,
'react/jsx-no-undef': 2,
'react/jsx-sort-prop-types': 0,
Expand All @@ -64,13 +46,87 @@ const jsRules = {
'react/react-in-jsx-scope': 2,
'react/self-closing-comp': 2,
'react/wrap-multilines': 0,
strict: 1,

// Formatting (stylistic) rules.
//
// 'Deprecated' in ESLint 8.53.0, see announcement
// - Announcement: https://eslint.org/blog/2023/10/deprecating-formatting-rules/
// - Rules deprecated: (https://github.com/eslint/eslint/commit/528e1c00dc2a)
//
// Successor: ESLint Stylistic, community project
// - Rules: https://eslint.style/packages/default#rules
//
// It'll be useful to keep a variant of these rules around as an alternative
// "auto-fixer" (versus Prettier), since Prettier modifies line wrappings in
// a way that can harm code readability at times.
//
// Idea:
// * Create a 'stylistic-only' ESLint config with these rules
// * Use the stylistic config for 'auto-fix' (lightweight formatting) in editor
// (normalize quotes, commas, semicolons, etc. without touching linebreaks)
// * Remove 'stylistic' rules from the main config, to reduce diagnostic
// clutter in code or in the 'Problems' pane
// * Keep an optional well-configured 'Prettier' tooling, able to run on
// modified lines or current/selected paragraph, around for convenience
'arrow-parens': [1, 'always'],
'arrow-spacing': 1,
'block-spacing': [1, 'never'],
'brace-style': [1, '1tbs', {allowSingleLine: true}],
'comma-dangle': [1, 'always-multiline'],
'comma-spacing': [1, {before: false, after: true}],
'eol-last': [1, 'always'],
'func-call-spacing': 1,
'jsx-quotes': ['warn', 'prefer-single'],
'key-spacing': [1, {beforeColon: false, afterColon: true, mode: 'strict'}],
'no-confusing-arrow': 1,
'no-extra-semi': 1,
'no-mixed-spaces-and-tabs': 1,
'no-multi-spaces': 1, // Comment out to permit columnar formatting
'no-multiple-empty-lines': [1, {max: 2, maxEOF: 1}],
'no-trailing-spaces': 1,
'no-whitespace-before-property': 1,
'quotes': ['warn', 'single', {avoidEscape: true}],
'semi-spacing': [1, {before: false, after: true}],
'semi-style': [1, 'last'],
semi: 1,
'semi': 1,
'space-before-function-paren': 'off',
'space-in-parens': [1, 'never'],
'space-infix-ops': 1,
strict: 1,

// React Plugin - Default to plugin:react/recommended.
// https://github.com/jsx-eslint/eslint-plugin-react

// TODO: Set these rules back to 'error' severity and fix the affected code
// (1) comment these out (2) run npx eslint (...) with --quiet to list only errors
// ===============================================================================
// Deprecated API
'react/no-deprecated': 1, // e.g. componentWillReceiveProps
'react/no-find-dom-node': 1, // Do not use findDOMNode
'react/no-string-refs': 1, // Using string literals in ref attributes is deprecated
// Misuse
'react/jsx-key': 1, // Missing "key" prop for element in iterator
'react/no-direct-mutation-state': 1, // Do not mutate state directly. Use setState()
// Other
'react/display-name': 1, // "Component definition is missing display name"
'react/no-unescaped-entities': 1, // e.g. `"` can be escaped with `"`…

// OK
// ==
'react/jsx-no-target-blank': 0, // Using target="_blank" without rel="noreferrer"
// …not a problem for modern browsers; see 2021 update
// (https://mathiasbynens.github.io/rel-noopener/#recommendations)


// React Hooks - Default to plugin:react-hooks/recommended
// https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks

// TODO: Fix these! They should be errors, too.
'react-hooks/rules-of-hooks': 1, // TODO: make this an error, not warning

// React Query - Default to plugin:@tanstack/eslint-plugin-query/recommended
// https://tanstack.com/query/latest/docs/eslint/eslint-plugin-query

};

// TypeScript rules override some of JavaScript rules plus add a few more.
Expand All @@ -79,19 +135,8 @@ const tsRules = Object.assign({}, jsRules, {
// Would be good to enable in future, when most of codebase is TS.
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/ban-ts-comment': 1,
'@typescript-eslint/comma-dangle': [1, 'always-multiline'],
'@typescript-eslint/comma-spacing': [1, {before: false, after: true}],
'@typescript-eslint/consistent-type-definitions': [1, 'interface'],
'@typescript-eslint/consistent-type-imports': [1, {prefer: 'type-imports'}],
'@typescript-eslint/func-call-spacing': [1, 'never'],
'@typescript-eslint/keyword-spacing': [1, {before: true, after: true}],
'@typescript-eslint/member-delimiter-style': [
1,
{
multiline: {delimiter: 'semi'},
singleline: {delimiter: 'semi'},
},
],
'@typescript-eslint/method-signature-style': [1, 'property'],
'@typescript-eslint/naming-convention': [
1,
Expand Down Expand Up @@ -127,8 +172,6 @@ const tsRules = Object.assign({}, jsRules, {
'@typescript-eslint/no-empty-function': 1,
'@typescript-eslint/no-empty-interface': 1,
'@typescript-eslint/no-explicit-any': 1,
'@typescript-eslint/no-extra-parens': 'off',
'@typescript-eslint/no-extra-semi': 1,
'@typescript-eslint/no-inferrable-types': 1,
'@typescript-eslint/no-invalid-void-type': 1,
'@typescript-eslint/no-loop-func': 1,
Expand All @@ -139,38 +182,11 @@ const tsRules = Object.assign({}, jsRules, {
'@typescript-eslint/no-use-before-define': 1,
'@typescript-eslint/no-useless-constructor': 1,
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/object-curly-spacing': 1,
'@typescript-eslint/parameter-properties': 1,
'@typescript-eslint/prefer-enum-initializers': 1,
'@typescript-eslint/prefer-optional-chain': 1,
'@typescript-eslint/quotes': ['warn', 'single', {avoidEscape: true}],
'@typescript-eslint/semi': 1,
'@typescript-eslint/sort-type-union-intersection-members': 'off',
'@typescript-eslint/space-before-function-paren': [
1,
{
anonymous: 'always',
named: 'never',
asyncArrow: 'always', // TODO: Defer all formatting rules to Prettier
},
],
'@typescript-eslint/type-annotation-spacing': [
1,
{
before: false,
after: true,
overrides: {
arrow: {
before: true,
after: true,
},
},
},
],
'@typescript-eslint/unified-signatures': 1,
'comma-dangle': 'off',
'comma-spacing': 'off',
'func-call-spacing': 'off',
// The 'import' plugin supports separately importing types
// (@typescript-eslint/no-duplicate-imports is deprecated)
'import/no-duplicates': 1,
Expand All @@ -185,8 +201,47 @@ const tsRules = Object.assign({}, jsRules, {
'no-unused-vars': 'off',
'no-useless-backreference': 'off',
'no-var': 1,
quotes: 'off',
semi: 'off',

// Formatting (stylistic) rules, for TypeScript.
// 'Deprecated' in @typescript-eslint v6.16.0 (https://typescript-eslint.io/blog/deprecating-formatting-rules/),
// Still useful for us in some form, particularly with auto-fix.
// See "Formatting (stylistic) rules" comment, above.
'@typescript-eslint/comma-dangle': [1, 'always-multiline'],
'@typescript-eslint/comma-spacing': [1, {before: false, after: true}],
'@typescript-eslint/func-call-spacing': [1, 'never'],
'@typescript-eslint/keyword-spacing': [1, {before: true, after: true}],
'@typescript-eslint/no-extra-parens': 'off',
'@typescript-eslint/no-extra-semi': 1,
'@typescript-eslint/object-curly-spacing': 1,
'@typescript-eslint/member-delimiter-style': [1, {
multiline: {delimiter: 'semi'},
singleline: {delimiter: 'semi'},
}],
'@typescript-eslint/quotes': ['warn', 'single', {avoidEscape: true}],
'@typescript-eslint/semi': 1,
'@typescript-eslint/type-annotation-spacing': [1, {
before: false,
after: true,
overrides: {
arrow: {
before: true,
after: true,
},
},
}],
'@typescript-eslint/space-before-function-paren': [1, {
anonymous: 'always',
named: 'never',
asyncArrow: 'always',
}],
// Turn off equivalent ESLint rules
'comma-dangle': 'off',
'comma-spacing': 'off',
'func-call-spacing': 'off',
'no-extra-semi': 'off',
'quotes': 'off',
'semi': 'off',

});

module.exports = {
Expand All @@ -206,12 +261,27 @@ module.exports = {
},
ignorePatterns: ['**/*.scss'],
plugins: ['react'],
extends: ['eslint:recommended', 'prettier', 'plugin:storybook/recommended'],
extends: [
'eslint:recommended',
'plugin:react/recommended', // Use recommended rules: https://github.com/jsx-eslint/eslint-plugin-react#list-of-supported-rules
'plugin:react/jsx-runtime', // Use the new JSX transform
'plugin:react-hooks/recommended', // Rules of Hooks (https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks)
'plugin:@tanstack/eslint-plugin-query/recommended', // For Tanstack Query (aka react-query)
'prettier',
'plugin:storybook/recommended'],
rules: jsRules,
settings: {
react: {
version: 'detect',
},
// https://github.com/jsx-eslint/eslint-plugin-react#configuration-legacy-eslintrc-
// Try to include all the component wrapper functions we use here
componentWrapperFunctions: [
'observer', // MobX observer
],
linkComponenets: [
{ name: 'Link', linkAttribute: 'to' }, // React Router Link
],
},
overrides: [
{
Expand Down
10 changes: 7 additions & 3 deletions jsapp/js/alertify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {KeyNames} from 'js/constants';
import type {IconName} from 'jsapp/fonts/k-icons';
import {escapeHtml} from 'js/utils';
import type {ReactElement} from 'react';
import {render} from 'react-dom';
import {createRoot} from 'react-dom/client';


interface MultiConfirmButton {
label: string;
Expand Down Expand Up @@ -193,6 +194,9 @@ export function destroyConfirm(
*/
export function renderJSXMessage(jsx: ReactElement) {
const domNode = document.createElement('div');
render(jsx, domNode);
return domNode.outerHTML;
const root = createRoot(domNode);
root.render(jsx);
const str = domNode.outerHTML;
root.unmount();
return str;
}
25 changes: 23 additions & 2 deletions jsapp/js/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ import {
import {isAnyProcessingRouteActive} from 'js/components/processing/routes.utils';
import pageState from 'js/pageState.store';

// Query-related
import { QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { queryClient } from './query/queryClient.ts'

class App extends React.Component {
constructor(props) {
super(props);
Expand Down Expand Up @@ -100,7 +105,7 @@ class App extends React.Component {
// opt for a more sane, and singluar(!) solution.
return (
<DocumentTitle title='KoboToolbox'>
<>
<QueryClientProvider client={queryClient}>
<RootContextProvider>
<Tracking />
<ToasterConfig />
Expand Down Expand Up @@ -139,7 +144,23 @@ class App extends React.Component {
</bem.PageWrapper__content>
</bem.PageWrapper>
</RootContextProvider>
</>


{/* React Query Devtools - GUI for inspecting and modifying query status
(https://tanstack.com/query/latest/docs/framework/react/devtools)
They only show up in dev server (NODE_ENV==='development')
Additionally, we're keeping them commented out in `beta`
(https://github.com/kobotoolbox/kpi/pull/5001#discussion_r1691067344)
(1) Uncomment if you want to use these tools
(2) The <style> tag lowers the toggle button opacity
to make it less prominent in dev screenshots. */}
{/*
<style>{'.tsqd-open-btn-container { opacity: 0.1 !important; };'}</style>
<ReactQueryDevtools />
*/}


</QueryClientProvider>
</DocumentTitle>
);
}
Expand Down
4 changes: 4 additions & 0 deletions jsapp/js/formbuild/renderInBackbone.es6
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,8 @@ export function renderKobomatrix (view, el) {
let model = new KoboMatrixRow(view.model);
const root = createRoot(el.get(0));
root.render(<KoboMatrix model={model} />);
// TODO: should this root be unmounted at some point?
// https://react.dev/reference/react-dom/client/createRoot#root-unmount
// Maybe instantiate the root in KoboMatrixView, then unmount it when
// KoboMatrixView is disposed.
}
8 changes: 8 additions & 0 deletions jsapp/js/query/queryClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { QueryClient } from '@tanstack/react-query'

// Some shared defaults and config can be set here!
// Docs: https://tanstack.com/query/v5/docs/reference/QueryClient#queryclient
// See: https://tanstack.com/query/v5/docs/framework/react/guides/important-defaults
const queryClient = new QueryClient()

export { queryClient }
Loading

0 comments on commit ee582a0

Please sign in to comment.