From 3865373a5b09f14fd86b4ba19e55e4fc86564946 Mon Sep 17 00:00:00 2001 From: Stig Ofstad Date: Wed, 22 May 2024 15:03:41 +0200 Subject: [PATCH] perf: use tanstack query to cache attribute and useDocument requests --- example/package.json | 1 + packages/dm-core/src/ApplicationContext.tsx | 6 +- .../components/ViewCreator/ViewCreator.tsx | 66 +++++---- packages/dm-core/src/hooks/useDocument.tsx | 129 ++++++++++-------- 4 files changed, 114 insertions(+), 88 deletions(-) diff --git a/example/package.json b/example/package.json index 3d6a85500..7d667078d 100644 --- a/example/package.json +++ b/example/package.json @@ -5,6 +5,7 @@ "dependencies": { "@development-framework/dm-core": "^1.3.3", "@development-framework/dm-core-plugins": "^1.3.3", + "@tanstack/react-query": "^5.37.1", "plotly.js": "^2.18.2", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/packages/dm-core/src/ApplicationContext.tsx b/packages/dm-core/src/ApplicationContext.tsx index 4273918fc..144954656 100644 --- a/packages/dm-core/src/ApplicationContext.tsx +++ b/packages/dm-core/src/ApplicationContext.tsx @@ -1,3 +1,4 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import React, { Dispatch, ReactNode, @@ -20,6 +21,7 @@ const DEFAULT_ROLE: TRole = { authServerRoleName: 'anonymous', label: 'Anonymous', } +const queryClient = new QueryClient() export const ApplicationContext = React.createContext< | { name: string @@ -153,7 +155,9 @@ export const DMApplicationProvider = (props: { loading, }} > - {props.children} + + {props.children} + ) diff --git a/packages/dm-core/src/components/ViewCreator/ViewCreator.tsx b/packages/dm-core/src/components/ViewCreator/ViewCreator.tsx index 801ef4553..dad07c1b7 100644 --- a/packages/dm-core/src/components/ViewCreator/ViewCreator.tsx +++ b/packages/dm-core/src/components/ViewCreator/ViewCreator.tsx @@ -1,7 +1,14 @@ import { Typography } from '@equinor/eds-core-react' -import { AxiosResponse } from 'axios' -import React, { useEffect, useMemo, useState } from 'react' -import { EntityView, Loading, TAttribute, useApplication } from '../../index' +import { useQuery } from '@tanstack/react-query' +import { AxiosError } from 'axios' +import React, { useMemo, useState } from 'react' +import { + EntityView, + ErrorResponse, + Loading, + TAttribute, + useApplication, +} from '../../index' import { IUIPlugin, TInlineRecipeViewConfig, @@ -40,31 +47,32 @@ type TViewCreator = Omit & { export const ViewCreator = (props: TViewCreator): React.ReactElement => { const { idReference, viewConfig, onOpen, onSubmit, onChange } = props const { dmssAPI } = useApplication() - const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState() - const [attribute, setAttribute] = useState() - const [directAddress, setDirectAddress] = useState() const reference = useMemo( () => getTarget(idReference, viewConfig), [idReference, viewConfig] ) + const queryKeys = ['attributes', reference, viewConfig.resolve] - useEffect(() => { - dmssAPI - .attributeGet({ - address: reference, - resolve: props.viewConfig.resolve, - }) - .then((response: AxiosResponse) => { - setAttribute(response.data.attribute) - setDirectAddress(response.data.address) - }) - .catch((error) => setError(error)) - .finally(() => setIsLoading(false)) - }, [reference]) + const { isPending, data } = useQuery<{ + address: string + attribute: TAttribute + }>({ + staleTime: 5 * 1000, + refetchOnMount: false, + queryKey: queryKeys, + queryFn: () => + dmssAPI + .attributeGet({ + address: reference, + resolve: props.viewConfig.resolve, + }) + .then((response: any) => response.data) + .catch((error: AxiosError) => setError(error)), + }) - if (isLoading || !directAddress) return + if (isPending || !data?.address) return if (error) return ( @@ -72,7 +80,7 @@ export const ViewCreator = (props: TViewCreator): React.ReactElement => { {error.message}) ) - if (attribute === undefined) + if (data.attribute === undefined) throw new Error('Unable to find type and dimensions for view') if (viewConfig === undefined) @@ -82,8 +90,8 @@ export const ViewCreator = (props: TViewCreator): React.ReactElement => { if (isInlineRecipeViewConfig(viewConfig)) { return ( { if (isReferenceViewConfig(viewConfig)) { return ( { } else if (isViewConfig(viewConfig)) { return ( { document: T | null isLoading: boolean @@ -55,68 +56,80 @@ export function useDocument( depth?: number | undefined, notify: boolean = true ): IUseDocumentReturnType { - const [document, setDocument] = useState(null) - const [isLoading, setLoading] = useState(true) - const [error, setError] = useState(null) const { dmssAPI } = useApplication() + const [errorResponse, setErrorResponse] = useState(null) + const queryClient = useQueryClient() - useEffect(() => { - setLoading(true) - const documentDepth: number = depth ?? 0 - if (documentDepth < 0 || documentDepth > 999) - throw new Error('Depth must be a positive number < 999') - dmssAPI - .documentGet({ - address: idReference, - depth: documentDepth, - }) - .then((response: any) => { - const data = response.data - setDocument(data) - setError(null) - }) - .catch((error: AxiosError) => { - console.error(error) - if (notify) - toast.error( - 'Unable to retrieve document, with message: ' + - error.response?.data.message ?? error.message + const queryKeys = ['documents', idReference, depth] + const documentDepth: number = depth ?? 0 + if (documentDepth < 0 || documentDepth > 999) + throw new Error('Depth must be a positive number < 999') + + const { isPending, data } = useQuery({ + staleTime: 5 * 1000, + refetchOnMount: false, + queryKey: queryKeys, + queryFn: () => + dmssAPI + .documentGet({ + address: idReference, + depth: documentDepth, + }) + .then((response: any) => response.data) + .catch((error: AxiosError) => { + console.error(error) + if (notify) + toast.error( + 'Unable to retrieve document, with message: ' + + error.response?.data.message ?? error.message + ) + setErrorResponse( + error.response?.data || { message: error.name, data: error } ) - setError(error.response?.data || { message: error.name, data: error }) - }) - .finally(() => setLoading(false)) - }, [idReference, depth]) + }), + }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async function updateDocument( - newDocument: T, - notify: boolean = true, - partialUpdate: boolean = false, - throwError: boolean = false - ): Promise { - setLoading(true) - return dmssAPI - .documentUpdate({ - idAddress: idReference, - data: JSON.stringify(newDocument), - partialUpdate: partialUpdate, - }) - .then((response: any) => { - const data = response.data.data - setDocument(data) - setError(null) - if (notify) toast.success('Document updated') - }) - .catch((error: AxiosError) => { - console.error(error) - if (notify) toast.error(error.response?.data.message ?? error.message) - setError(error.response?.data || { message: error.name, data: error }) - if (throwError) { + const mutation = useMutation({ + mutationFn: ({ + newDocument, + partialUpdate, + notify, + throwError, + }: { + newDocument: T + notify?: boolean + partialUpdate?: boolean + throwError?: boolean + }) => + dmssAPI + .documentUpdate({ + idAddress: idReference, + data: JSON.stringify(newDocument), + partialUpdate: partialUpdate, + }) + .then((response: any) => { + queryClient.setQueryData(queryKeys, response.data.data) + setErrorResponse(null) + if (notify) toast.success('Document updated') + }) + .catch((error: AxiosError) => { + console.error(error) + if (notify) toast.error(error.response?.data.message ?? error.message) + setErrorResponse( + error.response?.data || { message: error.name, data: error } + ) + if (throwError) { throw new Error(JSON.stringify(error, null, 2)) } - }) - .finally(() => setLoading(false)) - } + }), + }) - return { document, isLoading, updateDocument, error, setError } + return { + document: data || null, + isLoading: isPending, + updateDocument: (newDocument: T, notify, partialUpdate, throwError) => + mutation.mutateAsync({ newDocument, notify, partialUpdate, throwError }), + error: errorResponse, + setError: setErrorResponse + } }