Skip to content

Commit

Permalink
[1/2] [@graphiql/react] compile source code with react-compiler, remo…
Browse files Browse the repository at this point in the history
…ve `useMemo` and `useCallback` usages (#3821)

* aa

* aa

* aa

* aa

* aa

* aa

* aa

* aa

* aa

* aa

* aa

* aa

* aa

* [skip ci]

* fix

* migrate `graphiql` from `jest` to `vitest`

* add .js extension

* add .js extension

* upd

* upd

* upd

* upd

* upd

* upd

* upd

* upd

* upd

* upd
  • Loading branch information
dimaMachina authored Dec 14, 2024
1 parent efde1a0 commit 3633d61
Show file tree
Hide file tree
Showing 43 changed files with 1,128 additions and 827 deletions.
5 changes: 5 additions & 0 deletions .changeset/hip-cycles-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphiql/react': minor
---

compile source code with react-compiler, remove `useMemo` and `useCallback` usages
15 changes: 15 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,21 @@ module.exports = {
'promise/prefer-await-to-then': 'error',
},
},
{
files: ['packages/graphiql-react/**'],
plugins: ['react-compiler'],
rules: {
'@typescript-eslint/no-restricted-imports': [
'error',
...RESTRICTED_IMPORTS,
{
name: 'react',
importNames: ['memo', 'useCallback', 'useMemo'],
},
],
'react-compiler/react-compiler': 'error',
},
},
{
// Monaco-GraphQL rules
files: ['packages/monaco-graphql/**'],
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"gen-agenda": "wgutils agenda gen"
},
"dependencies": {
"eslint-plugin-react-compiler": "19.0.0-beta-37ed2a7-20241206",
"graphql-http": "^1.22.1",
"@babel/cli": "^7.21.0",
"@babel/core": "^7.21.0",
Expand Down Expand Up @@ -109,16 +110,16 @@
"concurrently": "^7.0.0",
"copy": "^0.3.2",
"cspell": "^5.15.2",
"eslint": "^9.7.0",
"eslint": "9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-cypress": "^3.4.0",
"eslint-plugin-import-x": "^3.1.0",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-mdx": "^3.1.5",
"eslint-plugin-promise": "^7.0.0",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^5.1.0-rc-76002254-20240724",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-sonar": "^0.14.1",
"eslint-plugin-sonarjs": "^1.0.4",
"eslint-plugin-unicorn": "^55.0.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/graphiql-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"react-dom": "^16.8.0 || ^17 || ^18"
},
"dependencies": {
"react-compiler-runtime": "19.0.0-beta-37ed2a7-20241206",
"@graphiql/toolkit": "^0.11.0",
"@headlessui/react": "^1.7.15",
"@radix-ui/react-dialog": "^1.0.4",
Expand All @@ -66,6 +67,7 @@
"set-value": "^4.1.0"
},
"devDependencies": {
"babel-plugin-react-compiler": "19.0.0-beta-37ed2a7-20241206",
"@types/react-dom": "^18.3.1",
"@babel/helper-string-parser": "^7.19.4",
"@testing-library/dom": "^10.4.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/graphiql-react/src/editor/common.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use no memo';

import { KeyMap } from './types';
import { isMacOs } from '../utility/is-macos';

Expand Down
2 changes: 2 additions & 0 deletions packages/graphiql-react/src/editor/completion.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use no memo';

import type { Editor, EditorChange } from 'codemirror';
import type { IHint } from 'codemirror-graphql/hint';
import {
Expand Down
2 changes: 2 additions & 0 deletions packages/graphiql-react/src/editor/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use no memo';

export { HeaderEditor } from './header-editor';
export { ImagePreview } from './image-preview';
export { QueryEditor } from './query-editor';
Expand Down
219 changes: 84 additions & 135 deletions packages/graphiql-react/src/editor/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,7 @@ import {
visit,
} from 'graphql';
import { VariableToType } from 'graphql-language-service';
import {
ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { ReactNode, useEffect, useRef, useState } from 'react';

import { useStorageContext } from '../storage';
import { createContextHook, createNullableContext } from '../utility/context';
Expand Down Expand Up @@ -329,7 +322,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) {

const [tabState, setTabState] = useState<TabsState>(initialState.tabState);

const setShouldPersistHeaders = useCallback(
const setShouldPersistHeaders = // eslint-disable-line react-hooks/exhaustive-deps -- false positive, function is optimized by react-compiler, no need to wrap with useCallback
(persist: boolean) => {
if (persist) {
storage?.set(STORAGE_KEY_HEADERS, headerEditor?.getValue() ?? '');
Expand All @@ -341,9 +334,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) {
}
setShouldPersistHeadersInternal(persist);
storage?.set(PERSIST_HEADERS_STORAGE_KEY, persist.toString());
},
[storage, tabState, headerEditor],
);
};

const lastShouldPersistHeadersProp = useRef<boolean | undefined>();
useEffect(() => {
Expand All @@ -369,7 +360,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) {
defaultHeaders,
});

const addTab = useCallback<EditorContextType['addTab']>(() => {
const addTab: EditorContextType['addTab'] = () => {
setTabState(current => {
// Make sure the current tab stores the latest values
const updatedValues = synchronizeActiveTabValues(current);
Expand All @@ -388,93 +379,71 @@ export function EditorContextProvider(props: EditorContextProviderProps) {
onTabChange?.(updated);
return updated;
});
}, [
defaultHeaders,
defaultQuery,
onTabChange,
setEditorValues,
storeTabs,
synchronizeActiveTabValues,
]);

const changeTab = useCallback<EditorContextType['changeTab']>(
index => {
setTabState(current => {
const updated = {
...current,
activeTabIndex: index,
};
storeTabs(updated);
setEditorValues(updated.tabs[updated.activeTabIndex]);
onTabChange?.(updated);
return updated;
});
},
[onTabChange, setEditorValues, storeTabs],
);
};

const moveTab = useCallback<EditorContextType['moveTab']>(
newOrder => {
setTabState(current => {
const activeTab = current.tabs[current.activeTabIndex];
const updated = {
tabs: newOrder,
activeTabIndex: newOrder.indexOf(activeTab),
};
storeTabs(updated);
setEditorValues(updated.tabs[updated.activeTabIndex]);
onTabChange?.(updated);
return updated;
});
},
[onTabChange, setEditorValues, storeTabs],
);
const changeTab: EditorContextType['changeTab'] = index => {
setTabState(current => {
const updated = {
...current,
activeTabIndex: index,
};
storeTabs(updated);
setEditorValues(updated.tabs[updated.activeTabIndex]);
onTabChange?.(updated);
return updated;
});
};

const closeTab = useCallback<EditorContextType['closeTab']>(
index => {
setTabState(current => {
const updated = {
tabs: current.tabs.filter((_tab, i) => index !== i),
activeTabIndex: Math.max(current.activeTabIndex - 1, 0),
};
storeTabs(updated);
setEditorValues(updated.tabs[updated.activeTabIndex]);
onTabChange?.(updated);
return updated;
});
},
[onTabChange, setEditorValues, storeTabs],
);
const moveTab: EditorContextType['moveTab'] = newOrder => {
setTabState(current => {
const activeTab = current.tabs[current.activeTabIndex];
const updated = {
tabs: newOrder,
activeTabIndex: newOrder.indexOf(activeTab),
};
storeTabs(updated);
setEditorValues(updated.tabs[updated.activeTabIndex]);
onTabChange?.(updated);
return updated;
});
};

const closeTab: EditorContextType['closeTab'] = index => {
setTabState(current => {
const updated = {
tabs: current.tabs.filter((_tab, i) => index !== i),
activeTabIndex: Math.max(current.activeTabIndex - 1, 0),
};
storeTabs(updated);
setEditorValues(updated.tabs[updated.activeTabIndex]);
onTabChange?.(updated);
return updated;
});
};

const updateActiveTabValues = useCallback<
EditorContextType['updateActiveTabValues']
>(
const updateActiveTabValues: EditorContextType['updateActiveTabValues'] =
partialTab => {
setTabState(current => {
const updated = setPropertiesInActiveTab(current, partialTab);
storeTabs(updated);
onTabChange?.(updated);
return updated;
});
},
[onTabChange, storeTabs],
);
};

const { onEditOperationName } = props;
const setOperationName = useCallback<EditorContextType['setOperationName']>(
const setOperationName: EditorContextType['setOperationName'] =
operationName => {
if (!queryEditor) {
return;
}

queryEditor.operationName = operationName;
updateQueryEditor(queryEditor, operationName);
updateActiveTabValues({ operationName });
onEditOperationName?.(operationName);
},
[onEditOperationName, queryEditor, updateActiveTabValues],
);
};

const externalFragments = useMemo(() => {
const externalFragments = (() => {
const map = new Map<string, FragmentDefinitionNode>();
if (Array.isArray(props.externalFragments)) {
for (const fragment of props.externalFragments) {
Expand All @@ -492,74 +461,54 @@ export function EditorContextProvider(props: EditorContextProviderProps) {
);
}
return map;
}, [props.externalFragments]);
})();

const validationRules = useMemo(
() => props.validationRules || [],
[props.validationRules],
);
const validationRules = props.validationRules || [];

const value = useMemo<EditorContextType>(
() => ({
...tabState,
addTab,
changeTab,
moveTab,
closeTab,
updateActiveTabValues,

headerEditor,
queryEditor,
responseEditor,
variableEditor,
setHeaderEditor,
setQueryEditor,
setResponseEditor,
setVariableEditor,

setOperationName,

initialQuery: initialState.query,
initialVariables: initialState.variables,
initialHeaders: initialState.headers,
initialResponse: initialState.response,

externalFragments,
validationRules,
const value: EditorContextType = {
...tabState,
addTab,
changeTab,
moveTab,
closeTab,
updateActiveTabValues,

shouldPersistHeaders,
setShouldPersistHeaders,
}),
[
tabState,
addTab,
changeTab,
moveTab,
closeTab,
updateActiveTabValues,

headerEditor,
queryEditor,
responseEditor,
variableEditor,
headerEditor,
queryEditor,
responseEditor,
variableEditor,
setHeaderEditor,
setQueryEditor,
setResponseEditor,
setVariableEditor,

setOperationName,
setOperationName,

initialState,
initialQuery: initialState.query,
initialVariables: initialState.variables,
initialHeaders: initialState.headers,
initialResponse: initialState.response,

externalFragments,
validationRules,
externalFragments,
validationRules,

shouldPersistHeaders,
setShouldPersistHeaders,
],
);
shouldPersistHeaders,
setShouldPersistHeaders,
};

return (
<EditorContext.Provider value={value}>{children}</EditorContext.Provider>
);
}

// To make react-compiler happy, otherwise it fails due mutating props
function updateQueryEditor(
queryEditor: CodeMirrorEditorWithOperationFacts,
operationName: string,
) {
queryEditor.operationName = operationName;
}

export const useEditorContext = createContextHook(EditorContext);

const PERSIST_HEADERS_STORAGE_KEY = 'shouldPersistHeaders';
Expand Down
Loading

0 comments on commit 3633d61

Please sign in to comment.