Skip to content

Commit

Permalink
Refactor React object memoization using custom hook
Browse files Browse the repository at this point in the history
**Why**:

- More concise
- Less error-prone in case of forgotten dependency key
- May encourage memoization of objects when appropriate, by making it less tedious to accomplish

changelog: Internal, Performance, Create helper utility for front-end object memoization
  • Loading branch information
aduth committed May 17, 2022
1 parent ca292a3 commit e314c44
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 54 deletions.
39 changes: 13 additions & 26 deletions app/javascript/packages/document-capture/context/acuant.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createContext, useContext, useMemo, useEffect, useState } from 'react';
import DeviceContext from './device';
import AnalyticsContext from './analytics';
import useObjectMemo from '@18f/identity-react-hooks/use-object-memo';

/** @typedef {import('react').ReactNode} ReactNode */

Expand Down Expand Up @@ -147,32 +148,18 @@ function AcuantContextProvider({
// types should treat camera as unsupported, since it's not relevant for Acuant SDK usage.
const [isCameraSupported, setIsCameraSupported] = useState(isMobile ? null : false);
const [isActive, setIsActive] = useState(false);
const value = useMemo(
() => ({
isReady,
isAcuantLoaded,
isError,
isCameraSupported,
isActive,
setIsActive,
endpoint,
credentials,
glareThreshold,
sharpnessThreshold,
}),
[
isReady,
isAcuantLoaded,
isError,
isCameraSupported,
isActive,
setIsActive,
endpoint,
credentials,
glareThreshold,
sharpnessThreshold,
],
);
const value = useObjectMemo({
isReady,
isAcuantLoaded,
isError,
isCameraSupported,
isActive,
setIsActive,
endpoint,
credentials,
glareThreshold,
sharpnessThreshold,
});

useEffect(() => {
// If state is already ready (via consideration of device type), skip loading Acuant SDK.
Expand Down
35 changes: 12 additions & 23 deletions app/javascript/packages/document-capture/context/upload.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createContext, useMemo } from 'react';
import { createContext } from 'react';
import { useObjectMemo } from '@18f/identity-react-hooks';
import defaultUpload from '../services/upload';

const UploadContext = createContext({
Expand Down Expand Up @@ -102,28 +103,16 @@ function UploadContextProvider({
? upload({ ...formData }, { endpoint: statusEndpoint, method, csrf })
: Promise.reject();

const value = useMemo(
() => ({
upload: uploadWithCSRF,
getStatus,
statusPollInterval,
backgroundUploadURLs,
backgroundUploadEncryptKey,
isMockClient,
flowPath,
csrf,
}),
[
upload,
getStatus,
statusPollInterval,
backgroundUploadURLs,
backgroundUploadEncryptKey,
isMockClient,
flowPath,
csrf,
],
);
const value = useObjectMemo({
upload: uploadWithCSRF,
getStatus,
statusPollInterval,
backgroundUploadURLs,
backgroundUploadEncryptKey,
isMockClient,
flowPath,
csrf,
});

return <UploadContext.Provider value={value}>{children}</UploadContext.Provider>;
}
Expand Down
1 change: 1 addition & 0 deletions app/javascript/packages/react-hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { default as useDidUpdateEffect } from './use-did-update-effect';
export { default as useIfStillMounted } from './use-if-still-mounted';
export { default as useImmutableCallback } from './use-immutable-callback';
export { default as useInstanceId } from './use-instance-id';
export { default as useObjectMemo } from './use-object-memo';
35 changes: 35 additions & 0 deletions app/javascript/packages/react-hooks/use-object-memo.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { renderHook } from '@testing-library/react-hooks';
import useObjectMemo from './use-object-memo';

describe('useObjectMemo', () => {
it('maintains reference over re-render for identical object values', () => {
const { rerender, result } = renderHook(({ object }) => useObjectMemo(object), {
initialProps: { object: { a: 1 } },
});

const { current: object1 } = result;

rerender({ object: { a: 1 } });

const { current: object2 } = result;

expect(object1).to.equal(object2);
expect(object1).to.deep.equal({ a: 1 });
});

it('updates reference when re-rendering with new values', () => {
const { rerender, result } = renderHook(({ object }) => useObjectMemo(object), {
initialProps: { object: { a: 1 } },
});

const { current: object1 } = result;
expect(object1).to.deep.equal({ a: 1 });

rerender({ object: { a: 2 } });

const { current: object2 } = result;
expect(object2).to.deep.equal({ a: 2 });

expect(object1).to.not.equal(object2);
});
});
20 changes: 20 additions & 0 deletions app/javascript/packages/react-hooks/use-object-memo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useMemo } from 'react';

/**
* React hook which creates a memoized object whose reference changes when values of the object
* changes.
*
* This can be useful in situations like object context values, since without memoization an object
* context would trigger re-renders of all consumers on every update.
*
* Note that the keys of the object must remain the same for the lifecycle of the component for the
* hook to work correctly.
*
* @param object Object to memoize.
*
* @return Memoized object.
*/
const useObjectMemo = <T extends object>(object: T): T =>
useMemo(() => object, Object.values(object));

export default useObjectMemo;
14 changes: 9 additions & 5 deletions app/javascript/packages/verify-flow/verify-flow.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useEffect, useState, useMemo } from 'react';
import { useEffect, useState } from 'react';
import { FormSteps } from '@18f/identity-form-steps';
import { trackEvent } from '@18f/identity-analytics';
import { getConfigValue } from '@18f/identity-config';
import { useObjectMemo } from '@18f/identity-react-hooks';
import { STEPS } from './steps';
import VerifyFlowStepIndicator from './verify-flow-step-indicator';
import VerifyFlowAlert from './verify-flow-alert';
Expand Down Expand Up @@ -112,10 +113,13 @@ function VerifyFlow({
const [syncedValues, setSyncedValues] = useSyncedSecretValues(initialValues);
const [currentStep, setCurrentStep] = useState(steps[0].name);
const [initialStep, setCompletedStep] = useInitialStepValidation(basePath, steps);
const context = useMemo(
() => ({ startOverURL, cancelURL, currentStep, basePath, resetPasswordUrl }),
[startOverURL, cancelURL, currentStep, basePath, resetPasswordUrl],
);
const context = useObjectMemo({
startOverURL,
cancelURL,
currentStep,
basePath,
resetPasswordUrl,
});
useEffect(() => {
logStepVisited(currentStep);
}, [currentStep]);
Expand Down

0 comments on commit e314c44

Please sign in to comment.