diff --git a/ui/package.json b/ui/package.json
index e0bb34221a8..afbf32896da 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -65,6 +65,7 @@
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"arg": "5.0.2",
+ "babel-plugin-react-compiler": "1.0.0",
"chromatic": "11.29.0",
"css-loader": "6.11.0",
"eslint": "8.45.0",
@@ -78,7 +79,7 @@
"eslint-plugin-prettier": "5.5.5",
"eslint-plugin-promise": "6.6.0",
"eslint-plugin-react": "7.37.5",
- "eslint-plugin-react-hooks": "4.6.2",
+ "eslint-plugin-react-hooks": "7.0.1",
"eslint-plugin-standard": "5.0.0",
"eslint-plugin-storybook": "0.12.0",
"eslint-plugin-typescript-enum": "2.1.0",
@@ -98,6 +99,7 @@
"postcss": "8.5.8",
"prettier": "3.6.2",
"prettier-plugin-tailwindcss": "^0.4.0",
+ "react-compiler-runtime": "1.0.0",
"react-is": "18.3.1",
"react-test-renderer": "18.3.1",
"replace-in-files": "3.0.0",
diff --git a/ui/packages/app/web/package.json b/ui/packages/app/web/package.json
index 3c0cf27713e..d3ca0e64b67 100644
--- a/ui/packages/app/web/package.json
+++ b/ui/packages/app/web/package.json
@@ -63,12 +63,14 @@
},
"devDependencies": {
"@types/lodash.throttle": "4.1.9",
- "@vitejs/plugin-react-swc": "3.11.0",
+ "@vitejs/plugin-react": "4.7.0",
+ "babel-plugin-react-compiler": "1.0.0",
"css-loader": "6.11.0",
"eslint-config-prettier": "8.10.2",
"eslint-plugin-import": "2.32.0",
"jest": "29.7.0",
"jest-runtime": "29.7.0",
+ "react-compiler-runtime": "1.0.0",
"tslint": "6.1.3",
"tslint-config-prettier": "1.18.0",
"tslint-plugin-prettier": "2.3.0",
diff --git a/ui/packages/app/web/src/components/ui/Navbar.tsx b/ui/packages/app/web/src/components/ui/Navbar.tsx
index d16f3935de0..0f8d0470fc3 100644
--- a/ui/packages/app/web/src/components/ui/Navbar.tsx
+++ b/ui/packages/app/web/src/components/ui/Navbar.tsx
@@ -61,7 +61,7 @@ const Navbar = () => {
const compareA = queryParams.get('compare_a');
const compareB = queryParams.get('compare_b');
- const queryParamsURL = parseParams(window.location.search);
+ const queryParamsURL = parseParams(location.search);
/* eslint-disable @typescript-eslint/naming-convention */
const {
diff --git a/ui/packages/app/web/src/pages/index.tsx b/ui/packages/app/web/src/pages/index.tsx
index 063d3c38d3d..a1af24e394a 100644
--- a/ui/packages/app/web/src/pages/index.tsx
+++ b/ui/packages/app/web/src/pages/index.tsx
@@ -14,7 +14,7 @@
import {useCallback} from 'react';
import {GrpcWebFetchTransport} from '@protobuf-ts/grpcweb-transport';
-import {useNavigate} from 'react-router-dom';
+import {useLocation, useNavigate} from 'react-router-dom';
import {QueryServiceClient} from '@parca/client';
import {ParcaContextProvider, Spinner, URLStateProvider} from '@parca/components';
@@ -31,7 +31,12 @@ const queryClient = new QueryServiceClient(
);
const Profiles = () => {
+ 'use no memo';
const navigate = useNavigate();
+ // useLocation() subscribes to react-router location changes so this component
+ // re-renders on navigate(). 'use no memo' ensures the re-render propagates to
+ // URLStateProvider, whose no-deps effect syncs state from window.location.search.
+ useLocation();
const isDarkMode = useAppSelector(selectDarkMode);
const navigateTo = useCallback(
diff --git a/ui/packages/app/web/vite.config.ts b/ui/packages/app/web/vite.config.ts
index 72a68fca6c4..6b1411786a0 100644
--- a/ui/packages/app/web/vite.config.ts
+++ b/ui/packages/app/web/vite.config.ts
@@ -11,14 +11,29 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import react from '@vitejs/plugin-react-swc';
+import react from '@vitejs/plugin-react';
import {defineConfig} from 'vite';
import svgr from 'vite-plugin-svgr';
// https://vitejs.dev/config/
export default defineConfig({
- // @ts-expect-error
- plugins: [react(), svgr()],
+ // cast needed: dual @types/node versions create incompatible vite Plugin types
+ plugins: [
+ react({
+ babel: {
+ plugins: [
+ [
+ 'babel-plugin-react-compiler',
+ {
+ target: '18',
+ compilationMode: 'infer',
+ },
+ ],
+ ],
+ },
+ }),
+ svgr(),
+ ] as any,
base: './',
server: {
port: 3000,
diff --git a/ui/packages/shared/components/src/DateTimeRangePicker/AbsoluteDatePicker/index.test.tsx b/ui/packages/shared/components/src/DateTimeRangePicker/AbsoluteDatePicker/index.test.tsx
new file mode 100644
index 00000000000..cad87483ac3
--- /dev/null
+++ b/ui/packages/shared/components/src/DateTimeRangePicker/AbsoluteDatePicker/index.test.tsx
@@ -0,0 +1,45 @@
+// Copyright 2022 The Parca Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/* eslint-disable jest-dom/prefer-to-have-value */
+
+import {render} from '@testing-library/react';
+import {describe, expect, it, vi} from 'vitest';
+
+import {AbsoluteDate, DateTimeRange} from '../utils';
+import AbsoluteDatePicker from './index';
+
+describe('AbsoluteDatePicker', () => {
+ it('resyncs when an existing DateTimeRange instance is mutated', () => {
+ const range = new DateTimeRange(
+ new AbsoluteDate(new Date('2023-12-01T10:00:00Z')),
+ new AbsoluteDate(new Date('2023-12-01T15:30:00Z'))
+ );
+
+ const {rerender, getAllByRole} = render(
+
+ );
+
+ const [startInput, endInput] = getAllByRole('textbox');
+ expect((startInput as HTMLInputElement).value).toBe('2023-12-01 10:00:00');
+ expect((endInput as HTMLInputElement).value).toBe('2023-12-01 15:30:00');
+
+ range.from = new AbsoluteDate(new Date('2023-12-02T08:15:00Z'));
+ range.to = new AbsoluteDate(new Date('2023-12-02T09:45:00Z'));
+
+ rerender();
+
+ expect((startInput as HTMLInputElement).value).toBe('2023-12-02 08:15:00');
+ expect((endInput as HTMLInputElement).value).toBe('2023-12-02 09:45:00');
+ });
+});
diff --git a/ui/packages/shared/components/src/DateTimeRangePicker/AbsoluteDatePicker/index.tsx b/ui/packages/shared/components/src/DateTimeRangePicker/AbsoluteDatePicker/index.tsx
index 6ad9206d586..5bdc43f3664 100644
--- a/ui/packages/shared/components/src/DateTimeRangePicker/AbsoluteDatePicker/index.tsx
+++ b/ui/packages/shared/components/src/DateTimeRangePicker/AbsoluteDatePicker/index.tsx
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {useEffect, useMemo, useState} from 'react';
+import {useState} from 'react';
import {DateTimePicker} from '../../DateTimePicker';
import {AbsoluteDate, DateTimeRange, RelativeDate, getHistoricalDate} from '../utils';
@@ -22,52 +22,24 @@ interface AbsoluteDatePickerProps {
}
const AbsoluteDatePicker = ({range, onChange}: AbsoluteDatePickerProps): JSX.Element => {
- const dateFromInRelative = useMemo(() => range.from as RelativeDate, [range.from]);
- const dateToInRelative = useMemo(() => range.to as RelativeDate, [range.to]);
-
- const [from, setFrom] = useState(
- range.from.isRelative()
- ? new AbsoluteDate(
- getHistoricalDate({
- unit: dateFromInRelative.unit,
- value: dateFromInRelative.value,
- })
- )
- : (range.from as AbsoluteDate)
- );
- const [to, setTo] = useState(
- range.to.isRelative()
+ const toAbsolute = (d: RelativeDate | AbsoluteDate): AbsoluteDate =>
+ d.isRelative()
? new AbsoluteDate(
- getHistoricalDate({
- unit: dateToInRelative.unit,
- value: dateToInRelative.value,
- })
+ getHistoricalDate({unit: (d as RelativeDate).unit, value: (d as RelativeDate).value})
)
- : (range.to as AbsoluteDate)
- );
+ : (d as AbsoluteDate);
+
+ const [from, setFrom] = useState(() => toAbsolute(range.from));
+ const [to, setTo] = useState(() => toAbsolute(range.to));
+ const [prevRangeFrom, setPrevRangeFrom] = useState(range.from);
+ const [prevRangeTo, setPrevRangeTo] = useState(range.to);
- useEffect(() => {
- setFrom(
- range.from.isRelative()
- ? new AbsoluteDate(
- getHistoricalDate({
- unit: dateFromInRelative.unit,
- value: dateFromInRelative.value,
- })
- )
- : (range.from as AbsoluteDate)
- );
- setTo(
- range.to.isRelative()
- ? new AbsoluteDate(
- getHistoricalDate({
- unit: dateToInRelative.unit,
- value: dateToInRelative.value,
- })
- )
- : (range.to as AbsoluteDate)
- );
- }, [dateFromInRelative, dateToInRelative, range.from, range.to]);
+ if (prevRangeFrom !== range.from || prevRangeTo !== range.to) {
+ setPrevRangeFrom(range.from);
+ setPrevRangeTo(range.to);
+ setFrom(toAbsolute(range.from));
+ setTo(toAbsolute(range.to));
+ }
return (
diff --git a/ui/packages/shared/components/src/ResponsiveSvg/index.tsx b/ui/packages/shared/components/src/ResponsiveSvg/index.tsx
index 6daec421439..f491b231822 100644
--- a/ui/packages/shared/components/src/ResponsiveSvg/index.tsx
+++ b/ui/packages/shared/components/src/ResponsiveSvg/index.tsx
@@ -11,6 +11,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+/* eslint-disable react-hooks/set-state-in-effect */
+
import {Children, useEffect, useState} from 'react';
import {useContainerDimensions} from '@parca/hooks';
@@ -33,6 +35,7 @@ const addPropsToChildren = (children: JSX.Element, props: {[x: string]: any}): J
};
const ResponsiveSvg = (props: Props): JSX.Element => {
+ 'use no memo';
const {children} = props;
const {ref, dimensions} = useContainerDimensions();
const {width} = dimensions ?? {width: 0};
diff --git a/ui/packages/shared/components/src/Table/index.tsx b/ui/packages/shared/components/src/Table/index.tsx
index f11d4aea0df..4c708675bfc 100644
--- a/ui/packages/shared/components/src/Table/index.tsx
+++ b/ui/packages/shared/components/src/Table/index.tsx
@@ -139,6 +139,7 @@ const Table = ({
scrollToIndex,
estimatedRowHeight = 26,
}: Props): JSX.Element => {
+ 'use no memo';
const [sorting, setSorting] = useState(initialSorting);
const tableContainerRef = useRef(null);
const scrollingRef = useRef();
diff --git a/ui/packages/shared/profile/src/GraphTooltipArrow/index.tsx b/ui/packages/shared/profile/src/GraphTooltipArrow/index.tsx
index 969d466fc6a..1fa035cba37 100644
--- a/ui/packages/shared/profile/src/GraphTooltipArrow/index.tsx
+++ b/ui/packages/shared/profile/src/GraphTooltipArrow/index.tsx
@@ -11,6 +11,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+/* eslint-disable react-hooks/refs */
+
import React, {useEffect, useState} from 'react';
import {flip, offset, shift, useFloating, type VirtualElement} from '@floating-ui/react';
@@ -39,6 +41,7 @@ function createPositionedVirtualElement(contextElement: Element, x = 0, y = 0):
}
const GraphTooltip = ({children, contextElement}: GraphTooltipProps): React.JSX.Element => {
+ 'use no memo';
const [isPositioned, setIsPositioned] = useState(false);
const {refs, floatingStyles, update} = useFloating({
diff --git a/ui/packages/shared/profile/src/MatchersInput/SuggestionsList.test.tsx b/ui/packages/shared/profile/src/MatchersInput/SuggestionsList.test.tsx
new file mode 100644
index 00000000000..50a08fcf8c7
--- /dev/null
+++ b/ui/packages/shared/profile/src/MatchersInput/SuggestionsList.test.tsx
@@ -0,0 +1,70 @@
+// Copyright 2022 The Parca Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {useRef} from 'react';
+
+import {act, fireEvent, render} from '@testing-library/react';
+import {beforeAll, describe, expect, it, vi} from 'vitest';
+
+import SuggestionsList, {Suggestion, Suggestions} from './SuggestionsList';
+
+vi.mock('@parca/components', () => ({
+ RefreshButton: ({title}: {title: string}) => ,
+ useParcaContext: () => ({
+ loader: