Skip to content

Commit 8cc45d2

Browse files
authored
feat(clerk-react,clerk-js,shared): Dynamically update props based on deep equality
* feat(clerk-react,clerk-js,shared): Dynamically update props based on deep equality This change was made in an effort to support dynamic values for appearance, localization as modifying this values usually happens during dev where a fast HMR iteration cycle is required. However, we'd also like to support dynamic values for props like redirectUrl, where the value can be calculated based on data available during runtime, even after the Clerk components mount (eg, based on the results of a fetch request). In order to avoid extra renders, we use the pre-existing dequal utility to make sure that expensive calculations are avoided. * Create pink-gifts-retire.md * fix(clerk-react,shared): Do not equally compare children and customPages React `children` can hold circular references with other `children` arrays and this crashes the deepEqual mechanism The customPages implementation requires the prop references to always change so there is no need to compare these on every prop update
1 parent c0a7455 commit 8cc45d2

8 files changed

Lines changed: 30 additions & 9 deletions

File tree

.changeset/pink-gifts-retire.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@clerk/clerk-js": patch
3+
"@clerk/clerk-react": patch
4+
"@clerk/shared": minor
5+
---
6+
7+
Allow dynamic values components props, even if these values change after the components are rendered. For example, a `SignIn` component with a `redirectUrl` prop passed in will always respect the latest value of `redirectUrl`.

packages/clerk-js/src/ui/customizables/AppearanceContext.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { createContextAndHook } from '@clerk/shared/react';
1+
import { createContextAndHook, useDeepEqualMemo } from '@clerk/shared/react';
22
import React from 'react';
33

4-
import { useDeepEqualMemo } from '../hooks';
54
import type { AppearanceCascade, ParsedAppearance } from './parseAppearance';
65
import { parseAppearance } from './parseAppearance';
76

packages/clerk-js/src/ui/hooks/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,5 @@ export * from './useSafeState';
1717
export * from './useSearchInput';
1818
export * from './useDebounce';
1919
export * from './useScrollLock';
20-
export * from './useDeepEqualMemo';
2120
export * from './useClerkModalStateParams';
2221
export * from './useNavigateToFlowStart';

packages/react/src/components/uiComponents.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { logErrorInDevMode } from '@clerk/shared';
1+
import { logErrorInDevMode, without } from '@clerk/shared';
2+
import { isDeeplyEqual } from '@clerk/shared/react';
23
import type {
34
CreateOrganizationProps,
45
OrganizationListProps,
@@ -89,11 +90,15 @@ type OrganizationSwitcherPropsWithoutCustomPages = Without<OrganizationSwitcherP
8990
class Portal extends React.PureComponent<MountProps> {
9091
private portalRef = React.createRef<HTMLDivElement>();
9192

92-
componentDidUpdate(prevProps: Readonly<MountProps>) {
93-
if (
94-
prevProps.props.appearance !== this.props.props.appearance ||
95-
prevProps.props?.customPages?.length !== this.props.props?.customPages?.length
96-
) {
93+
componentDidUpdate(_prevProps: Readonly<MountProps>) {
94+
// Remove children and customPages from props before comparing
95+
// children might hold circular references which deepEqual can't handle
96+
// and the implementation of customPages relies on props getting new references
97+
const prevProps = without(_prevProps.props, 'customPages', 'children');
98+
const newProps = without(this.props.props, 'customPages', 'children');
99+
// instead, we simply use the length of customPages to determine if it changed or not
100+
const customPagesChanged = prevProps.customPages?.length !== newProps.customPages?.length;
101+
if (!isDeeplyEqual(prevProps, newProps) || customPagesChanged) {
97102
this.props.updateProps({ node: this.portalRef.current, props: this.props.props });
98103
}
99104
}

packages/shared/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ export * from './poller';
2929
export * from './proxy';
3030
export * from './underscore';
3131
export * from './url';
32+
export * from './object';
3233
export { createWorkerTimers } from './workerTimers';
3334
export { DEV_BROWSER_JWT_KEY, getDevBrowserJWTFromURL, setDevBrowserJWTInURL } from './devBrowser';

packages/shared/src/object.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const without = <T extends object, P extends keyof T>(obj: T, ...props: P[]): Omit<T, P> => {
2+
const copy = { ...obj };
3+
for (const prop of props) {
4+
delete copy[prop];
5+
}
6+
return copy;
7+
};

packages/shared/src/react/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export { useSession } from './useSession';
66
export { useSessionList } from './useSessionList';
77
export { useUser } from './useUser';
88
export { useClerk } from './useClerk';
9+
export { useDeepEqualMemo, isDeeplyEqual } from './useDeepEqualMemo';

packages/clerk-js/src/ui/hooks/useDeepEqualMemo.ts renamed to packages/shared/src/react/hooks/useDeepEqualMemo.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ const useDeepEqualMemoize = <T>(value: T) => {
1616
export const useDeepEqualMemo: UseDeepEqualMemo = (factory, dependencyArray) => {
1717
return React.useMemo(factory, useDeepEqualMemoize(dependencyArray));
1818
};
19+
20+
export const isDeeplyEqual = deepEqual;

0 commit comments

Comments
 (0)