diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate.tsx index 4c1c3e0a0d..279ae86ef0 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredQueryParam } from 'hooks/useRequiredQueryParam'; -import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; import { FeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm'; import FormTemplate from 'component/common/FormTemplate/FormTemplate'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; @@ -23,6 +22,7 @@ import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/perm import { ISegment } from 'interfaces/segment'; import { useSegmentsApi } from 'hooks/api/actions/useSegmentsApi/useSegmentsApi'; import { formatStrategyName } from 'utils/strategyNames'; +import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmutable'; export const FeatureStrategyCreate = () => { const projectId = useRequiredPathParam('projectId'); @@ -35,12 +35,16 @@ export const FeatureStrategyCreate = () => { const { addStrategyToFeature, loading } = useFeatureStrategyApi(); const { setStrategySegments } = useSegmentsApi(); - const { feature, refetchFeature } = useFeature(projectId, featureId); const { setToastData, setToastApiError } = useToast(); const { uiConfig } = useUiConfig(); const { unleashUrl } = uiConfig; const { push } = useHistory(); + const { feature, refetchFeature } = useFeatureImmutable( + projectId, + featureId + ); + useEffect(() => { // Fill in the default values once the strategies have been fetched. setStrategy(getStrategyObject(strategies, strategyName, featureId)); diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit.tsx index dc37a1823f..a729171551 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from 'react'; -import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; import { FeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm'; import FormTemplate from 'component/common/FormTemplate/FormTemplate'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; @@ -15,6 +14,7 @@ import { ISegment } from 'interfaces/segment'; import { useSegmentsApi } from 'hooks/api/actions/useSegmentsApi/useSegmentsApi'; import { useSegments } from 'hooks/api/getters/useSegments/useSegments'; import { formatStrategyName } from 'utils/strategyNames'; +import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmutable'; export const FeatureStrategyEdit = () => { const projectId = useRequiredPathParam('projectId'); @@ -25,13 +25,17 @@ export const FeatureStrategyEdit = () => { const [strategy, setStrategy] = useState>({}); const [segments, setSegments] = useState([]); const { updateStrategyOnFeature, loading } = useFeatureStrategyApi(); - const { feature, refetchFeature } = useFeature(projectId, featureId); const { setStrategySegments } = useSegmentsApi(); const { setToastData, setToastApiError } = useToast(); const { uiConfig } = useUiConfig(); const { unleashUrl } = uiConfig; const { push } = useHistory(); + const { feature, refetchFeature } = useFeatureImmutable( + projectId, + featureId + ); + const { segments: savedStrategySegments, refetchSegments: refetchSavedStrategySegments, diff --git a/frontend/src/hooks/api/getters/useFeature/useFeature.ts b/frontend/src/hooks/api/getters/useFeature/useFeature.ts index ef23eacce5..ae10fb4dd2 100644 --- a/frontend/src/hooks/api/getters/useFeature/useFeature.ts +++ b/frontend/src/hooks/api/getters/useFeature/useFeature.ts @@ -1,11 +1,11 @@ -import useSWR, { mutate, SWRConfiguration } from 'swr'; +import useSWR, { SWRConfiguration } from 'swr'; import { useCallback } from 'react'; import { emptyFeature } from './emptyFeature'; import handleErrorResponses from '../httpErrorResponseHandler'; import { formatApiPath } from 'utils/formatPath'; import { IFeatureToggle } from 'interfaces/featureToggle'; -interface IUseFeatureOutput { +export interface IUseFeatureOutput { feature: IFeatureToggle; refetchFeature: () => void; loading: boolean; @@ -13,7 +13,7 @@ interface IUseFeatureOutput { error?: Error; } -interface IFeatureResponse { +export interface IFeatureResponse { status: number; body?: IFeatureToggle; } @@ -23,19 +23,17 @@ export const useFeature = ( featureId: string, options?: SWRConfiguration ): IUseFeatureOutput => { - const path = formatApiPath( - `api/admin/projects/${projectId}/features/${featureId}` - ); + const path = formatFeatureApiPath(projectId, featureId); - const { data, error } = useSWR( - path, - () => fetcher(path), + const { data, error, mutate } = useSWR( + ['useFeature', path], + () => featureFetcher(path), options ); const refetchFeature = useCallback(() => { - mutate(path).catch(console.warn); - }, [path]); + mutate().catch(console.warn); + }, [mutate]); return { feature: data?.body || emptyFeature, @@ -46,7 +44,9 @@ export const useFeature = ( }; }; -const fetcher = async (path: string): Promise => { +export const featureFetcher = async ( + path: string +): Promise => { const res = await fetch(path); if (res.status === 404) { @@ -62,3 +62,12 @@ const fetcher = async (path: string): Promise => { body: await res.json(), }; }; + +export const formatFeatureApiPath = ( + projectId: string, + featureId: string +): string => { + return formatApiPath( + `api/admin/projects/${projectId}/features/${featureId}` + ); +}; diff --git a/frontend/src/hooks/api/getters/useFeature/useFeatureImmutable.ts b/frontend/src/hooks/api/getters/useFeature/useFeatureImmutable.ts new file mode 100644 index 0000000000..fc60931950 --- /dev/null +++ b/frontend/src/hooks/api/getters/useFeature/useFeatureImmutable.ts @@ -0,0 +1,36 @@ +import useSWRImmutable from 'swr/immutable'; +import { useCallback } from 'react'; +import { emptyFeature } from './emptyFeature'; +import { + IUseFeatureOutput, + IFeatureResponse, + featureFetcher, + formatFeatureApiPath, +} from 'hooks/api/getters/useFeature/useFeature'; + +// useFeatureImmutable is like useFeature, except it won't refetch data on +// focus/reconnect/remount. Useful for
s that need a stable copy of +// the data. In particular, the lastSeenAt field may often change. +export const useFeatureImmutable = ( + projectId: string, + featureId: string +): IUseFeatureOutput => { + const path = formatFeatureApiPath(projectId, featureId); + + const { data, error, mutate } = useSWRImmutable( + ['useFeatureImmutable', path], + () => featureFetcher(path) + ); + + const refetchFeature = useCallback(() => { + mutate().catch(console.warn); + }, [mutate]); + + return { + feature: data?.body || emptyFeature, + refetchFeature, + loading: !error && !data, + status: data?.status, + error, + }; +}; diff --git a/frontend/src/hooks/api/getters/useFeatureEvents/useFeatureEvents.ts b/frontend/src/hooks/api/getters/useFeatureEvents/useFeatureEvents.ts index d78957218d..e5ea6d2b0d 100644 --- a/frontend/src/hooks/api/getters/useFeatureEvents/useFeatureEvents.ts +++ b/frontend/src/hooks/api/getters/useFeatureEvents/useFeatureEvents.ts @@ -1,4 +1,4 @@ -import useSWR, { mutate, SWRConfiguration } from 'swr'; +import useSWR, { SWRConfiguration } from 'swr'; import { useCallback } from 'react'; import { formatApiPath } from 'utils/formatPath'; import handleErrorResponses from '../httpErrorResponseHandler'; @@ -17,15 +17,15 @@ export const useFeatureEvents = ( featureName: string, options?: SWRConfiguration ): IUseEventsOutput => { - const { data, error } = useSWR<{ events: IEvent[] }>( + const { data, error, mutate } = useSWR<{ events: IEvent[] }>( [PATH, featureName], () => fetchFeatureEvents(featureName), options ); const refetchEvents = useCallback(() => { - mutate(PATH).catch(console.warn); - }, []); + mutate().catch(console.warn); + }, [mutate]); return { events: data?.events || [],