From 8bd518048e03be905c1f94a222091807c5d74586 Mon Sep 17 00:00:00 2001
From: DaneEveritt
Date: Mon, 20 Jun 2022 13:19:40 -0400
Subject: [PATCH] Fix excessive re-rendering due to route changesd
---
resources/scripts/api/server/activity.ts | 2 +-
.../activity/ActivityLogContainer.tsx | 11 +++----
.../elements/activity/ActivityLogEntry.tsx | 16 +++-------
.../components/elements/button/index.ts | 1 -
.../server/ServerActivityLogContainer.tsx | 8 ++---
resources/scripts/plugins/useLocationHash.ts | 30 +++++++++++++++++++
.../scripts/plugins/useUserSWRContentKey.ts | 10 +++++--
7 files changed, 51 insertions(+), 27 deletions(-)
create mode 100644 resources/scripts/plugins/useLocationHash.ts
diff --git a/resources/scripts/api/server/activity.ts b/resources/scripts/api/server/activity.ts
index 4c195b3df..880496e46 100644
--- a/resources/scripts/api/server/activity.ts
+++ b/resources/scripts/api/server/activity.ts
@@ -11,7 +11,7 @@ export type ActivityLogFilters = QueryBuilderParams<'ip' | 'event', 'timestamp'>
const useActivityLogs = (filters?: ActivityLogFilters, config?: ConfigInterface, AxiosError>): responseInterface, AxiosError> => {
const uuid = ServerContext.useStoreState(state => state.server.data?.uuid);
- const key = useUserSWRContentKey([ 'server', 'activity', JSON.stringify(useFilteredObject(filters || {})) ]);
+ const key = useUserSWRContentKey([ 'server', 'activity', useFilteredObject(filters || {}) ]);
return useSWR>(key, async () => {
const { data } = await http.get(`/api/client/servers/${uuid}/activity`, {
diff --git a/resources/scripts/components/dashboard/activity/ActivityLogContainer.tsx b/resources/scripts/components/dashboard/activity/ActivityLogContainer.tsx
index 0e9656fcc..698264e9b 100644
--- a/resources/scripts/components/dashboard/activity/ActivityLogContainer.tsx
+++ b/resources/scripts/components/dashboard/activity/ActivityLogContainer.tsx
@@ -6,16 +6,15 @@ import FlashMessageRender from '@/components/FlashMessageRender';
import { Link } from 'react-router-dom';
import PaginationFooter from '@/components/elements/table/PaginationFooter';
import { DesktopComputerIcon, XCircleIcon } from '@heroicons/react/solid';
-import { useLocation } from 'react-router';
import Spinner from '@/components/elements/Spinner';
import { styles as btnStyles } from '@/components/elements/button/index';
import classNames from 'classnames';
import ActivityLogEntry from '@/components/elements/activity/ActivityLogEntry';
import Tooltip from '@/components/elements/tooltip/Tooltip';
+import useLocationHash from '@/plugins/useLocationHash';
export default () => {
- const location = useLocation();
-
+ const { hash } = useLocationHash();
const { clearAndAddHttpError } = useFlashKey('account');
const [ filters, setFilters ] = useState({ page: 1, sorts: { timestamp: -1 } });
const { data, isValidating, error } = useActivityLogs(filters, {
@@ -24,10 +23,8 @@ export default () => {
});
useEffect(() => {
- const parsed = new URLSearchParams(location.search);
-
- setFilters(value => ({ ...value, filters: { ip: parsed.get('ip'), event: parsed.get('event') } }));
- }, [ location.search ]);
+ setFilters(value => ({ ...value, filters: { ip: hash.ip, event: hash.event } }));
+ }, [ hash ]);
useEffect(() => {
clearAndAddHttpError(error);
diff --git a/resources/scripts/components/elements/activity/ActivityLogEntry.tsx b/resources/scripts/components/elements/activity/ActivityLogEntry.tsx
index c68fcea24..ae528532c 100644
--- a/resources/scripts/components/elements/activity/ActivityLogEntry.tsx
+++ b/resources/scripts/components/elements/activity/ActivityLogEntry.tsx
@@ -4,13 +4,13 @@ import Tooltip from '@/components/elements/tooltip/Tooltip';
import Translate from '@/components/elements/Translate';
import { format, formatDistanceToNowStrict } from 'date-fns';
import { ActivityLog } from '@definitions/user';
-import { useLocation } from 'react-router';
import ActivityLogMetaButton from '@/components/elements/activity/ActivityLogMetaButton';
import { TerminalIcon } from '@heroicons/react/solid';
import classNames from 'classnames';
import style from './style.module.css';
import { isObject } from '@/helpers';
import Avatar from '@/components/Avatar';
+import useLocationHash from '@/plugins/useLocationHash';
interface Props {
activity: ActivityLog;
@@ -33,16 +33,8 @@ const formatProperties = (properties: Record): Record {
- const location = useLocation();
+ const { pathTo } = useLocationHash();
const actor = activity.relationships.actor;
-
- const queryTo = (params: Record): string => {
- const current = new URLSearchParams(location.search);
- Object.keys(params).forEach(key => current.set(key, params[key]));
-
- return current.toString();
- };
-
const properties = formatProperties(activity.properties);
return (
@@ -60,7 +52,7 @@ export default ({ activity, children }: Props) => {
—
{activity.event}
@@ -79,7 +71,7 @@ export default ({ activity, children }: Props) => {
{activity.ip}
diff --git a/resources/scripts/components/elements/button/index.ts b/resources/scripts/components/elements/button/index.ts
index dd56f8ad2..f40f42db6 100644
--- a/resources/scripts/components/elements/button/index.ts
+++ b/resources/scripts/components/elements/button/index.ts
@@ -1,3 +1,2 @@
-export { ButtonProps } from './types';
export { default as Button } from './Button';
export { default as styles } from './style.module.css';
diff --git a/resources/scripts/components/server/ServerActivityLogContainer.tsx b/resources/scripts/components/server/ServerActivityLogContainer.tsx
index 63efea949..dcbfb995a 100644
--- a/resources/scripts/components/server/ServerActivityLogContainer.tsx
+++ b/resources/scripts/components/server/ServerActivityLogContainer.tsx
@@ -11,8 +11,10 @@ import { Link } from 'react-router-dom';
import classNames from 'classnames';
import { styles as btnStyles } from '@/components/elements/button/index';
import { XCircleIcon } from '@heroicons/react/solid';
+import useLocationHash from '@/plugins/useLocationHash';
export default () => {
+ const { hash } = useLocationHash();
const { clearAndAddHttpError } = useFlashKey('server:activity');
const [ filters, setFilters ] = useState
({ page: 1, sorts: { timestamp: -1 } });
@@ -22,10 +24,8 @@ export default () => {
});
useEffect(() => {
- const parsed = new URLSearchParams(location.search);
-
- setFilters(value => ({ ...value, filters: { ip: parsed.get('ip'), event: parsed.get('event') } }));
- }, [ location.search ]);
+ setFilters(value => ({ ...value, filters: { ip: hash.ip, event: hash.event } }));
+ }, [ hash ]);
useEffect(() => {
clearAndAddHttpError(error);
diff --git a/resources/scripts/plugins/useLocationHash.ts b/resources/scripts/plugins/useLocationHash.ts
new file mode 100644
index 000000000..ff0ac7e41
--- /dev/null
+++ b/resources/scripts/plugins/useLocationHash.ts
@@ -0,0 +1,30 @@
+import { useLocation } from 'react-router';
+import { useMemo } from 'react';
+
+export default () => {
+ const location = useLocation();
+
+ const getHashObject = (value: string): Record =>
+ value
+ .substring(1)
+ .split('&')
+ .reduce((obj, str) => {
+ const [ key, value = '' ] = str.split('=');
+
+ return !str.trim() ? obj : { ...obj, [key]: value };
+ }, {});
+
+ const pathTo = (params: Record): string => {
+ const current = getHashObject(location.hash);
+
+ for (const key in params) {
+ current[key] = params[key];
+ }
+
+ return Object.keys(current).map(key => `${key}=${current[key]}`).join('&');
+ };
+
+ const hash = useMemo((): Record => getHashObject(location.hash), [ location.hash ]);
+
+ return { hash, pathTo };
+};
diff --git a/resources/scripts/plugins/useUserSWRContentKey.ts b/resources/scripts/plugins/useUserSWRContentKey.ts
index 23ea2eca2..a825b5ed0 100644
--- a/resources/scripts/plugins/useUserSWRContentKey.ts
+++ b/resources/scripts/plugins/useUserSWRContentKey.ts
@@ -1,8 +1,14 @@
import { useStoreState } from '@/state/hooks';
+import { useDeepCompareMemo } from '@/plugins/useDeepCompareMemo';
-export default (context: string | string[]) => {
- const key = Array.isArray(context) ? context.join(':') : context;
+// eslint-disable-next-line @typescript-eslint/ban-types
+export default (context: string | string[] | (string | number | null | {})[]) => {
const uuid = useStoreState(state => state.user.data?.uuid);
+ const key = useDeepCompareMemo((): string => {
+ return (Array.isArray(context) ? context : [ context ])
+ .map((value) => JSON.stringify(value))
+ .join(':');
+ }, [ context ]);
if (!key.trim().length) {
throw new Error('Must provide a valid context key to "useUserSWRContextKey".');