diff --git a/.changeset/pink-gifts-retire.md b/.changeset/pink-gifts-retire.md new file mode 100644 index 0000000000..8de046f04c --- /dev/null +++ b/.changeset/pink-gifts-retire.md @@ -0,0 +1,7 @@ +--- +"@clerk/clerk-js": patch +"@clerk/clerk-react": patch +"@clerk/shared": minor +--- + +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`. diff --git a/packages/clerk-js/src/ui/customizables/AppearanceContext.tsx b/packages/clerk-js/src/ui/customizables/AppearanceContext.tsx index 201edc9cab..dfedab2507 100644 --- a/packages/clerk-js/src/ui/customizables/AppearanceContext.tsx +++ b/packages/clerk-js/src/ui/customizables/AppearanceContext.tsx @@ -1,7 +1,6 @@ -import { createContextAndHook } from '@clerk/shared/react'; +import { createContextAndHook, useDeepEqualMemo } from '@clerk/shared/react'; import React from 'react'; -import { useDeepEqualMemo } from '../hooks'; import type { AppearanceCascade, ParsedAppearance } from './parseAppearance'; import { parseAppearance } from './parseAppearance'; diff --git a/packages/clerk-js/src/ui/hooks/index.ts b/packages/clerk-js/src/ui/hooks/index.ts index 5679be23e3..06141362e1 100644 --- a/packages/clerk-js/src/ui/hooks/index.ts +++ b/packages/clerk-js/src/ui/hooks/index.ts @@ -17,6 +17,5 @@ export * from './useSafeState'; export * from './useSearchInput'; export * from './useDebounce'; export * from './useScrollLock'; -export * from './useDeepEqualMemo'; export * from './useClerkModalStateParams'; export * from './useNavigateToFlowStart'; diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx index ce9033f901..d3f1f324fd 100644 --- a/packages/react/src/components/uiComponents.tsx +++ b/packages/react/src/components/uiComponents.tsx @@ -1,4 +1,5 @@ -import { logErrorInDevMode } from '@clerk/shared'; +import { logErrorInDevMode, without } from '@clerk/shared'; +import { isDeeplyEqual } from '@clerk/shared/react'; import type { CreateOrganizationProps, OrganizationListProps, @@ -89,11 +90,15 @@ type OrganizationSwitcherPropsWithoutCustomPages = Without { private portalRef = React.createRef(); - componentDidUpdate(prevProps: Readonly) { - if ( - prevProps.props.appearance !== this.props.props.appearance || - prevProps.props?.customPages?.length !== this.props.props?.customPages?.length - ) { + componentDidUpdate(_prevProps: Readonly) { + // Remove children and customPages from props before comparing + // children might hold circular references which deepEqual can't handle + // and the implementation of customPages relies on props getting new references + const prevProps = without(_prevProps.props, 'customPages', 'children'); + const newProps = without(this.props.props, 'customPages', 'children'); + // instead, we simply use the length of customPages to determine if it changed or not + const customPagesChanged = prevProps.customPages?.length !== newProps.customPages?.length; + if (!isDeeplyEqual(prevProps, newProps) || customPagesChanged) { this.props.updateProps({ node: this.portalRef.current, props: this.props.props }); } } diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index e1cfa05526..0a148aa790 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -29,5 +29,6 @@ export * from './poller'; export * from './proxy'; export * from './underscore'; export * from './url'; +export * from './object'; export { createWorkerTimers } from './workerTimers'; export { DEV_BROWSER_JWT_KEY, getDevBrowserJWTFromURL, setDevBrowserJWTInURL } from './devBrowser'; diff --git a/packages/shared/src/object.ts b/packages/shared/src/object.ts new file mode 100644 index 0000000000..e47d399f20 --- /dev/null +++ b/packages/shared/src/object.ts @@ -0,0 +1,7 @@ +export const without = (obj: T, ...props: P[]): Omit => { + const copy = { ...obj }; + for (const prop of props) { + delete copy[prop]; + } + return copy; +}; diff --git a/packages/shared/src/react/hooks/index.ts b/packages/shared/src/react/hooks/index.ts index b0d9779bc8..b55def5947 100644 --- a/packages/shared/src/react/hooks/index.ts +++ b/packages/shared/src/react/hooks/index.ts @@ -6,3 +6,4 @@ export { useSession } from './useSession'; export { useSessionList } from './useSessionList'; export { useUser } from './useUser'; export { useClerk } from './useClerk'; +export { useDeepEqualMemo, isDeeplyEqual } from './useDeepEqualMemo'; diff --git a/packages/clerk-js/src/ui/hooks/useDeepEqualMemo.ts b/packages/shared/src/react/hooks/useDeepEqualMemo.ts similarity index 94% rename from packages/clerk-js/src/ui/hooks/useDeepEqualMemo.ts rename to packages/shared/src/react/hooks/useDeepEqualMemo.ts index d86a60666b..9eb8f0ca7d 100644 --- a/packages/clerk-js/src/ui/hooks/useDeepEqualMemo.ts +++ b/packages/shared/src/react/hooks/useDeepEqualMemo.ts @@ -16,3 +16,5 @@ const useDeepEqualMemoize = (value: T) => { export const useDeepEqualMemo: UseDeepEqualMemo = (factory, dependencyArray) => { return React.useMemo(factory, useDeepEqualMemoize(dependencyArray)); }; + +export const isDeeplyEqual = deepEqual;