3 June 2025
Composable Commerce
Merchant Center Customizations
Announcement
DevelopmentGeneralConfiguration
The Application Kit packages have been released with a new major version v24, and the UI Kit packages with a new major version v20.

This guide provides detailed information about the migration process and necessary changes for Custom Applications and Custom Views to ensure a successful migration.

This release contains breaking changes.

Preparation

Before starting, we recommend that you understand the key changes introduced in the relevant React major versions.

You can find detailed information in the official upgrade guides:

Dependencies

We updated the following dependencies (besides React) to a major version:

  • react-intl: v7.1.4
  • @emotion/react: v11.14.0
  • downshift: v8.5.0
  • @testing-library/react: v16.1.0
  • @testing-library/dom: v10.4.0

We replaced the following third-party libraries because they don't support newer versions of React:

  • react-beautiful-dnd was replaced with @hello-pangea/dnd
  • react-sortable-hoc was replaced with @hello-pangea/dnd

Testing

Given the update of both react and react-testing-library dependencies, we had to refactor several patterns in our tests.

We don't use the @testing-library/react-hooks dependency anymore.
When working with hooks we can import the renderHook utility function directly from @testing-library/react package.

This utility can be referenced in two ways:

  1. From the @testing-library/react package
  2. From the @commercetools-frontend/application-shell/test-utils package
    • We should use this one only if our hook depends on the application context or other information stored in any provider of the application shell
The new renderHook function does not return the waitUntilReady helper as part of its return value. We had to replace their usages by relying on the waitFor function exported from the @testing-library/react package.

In some tests where we were expecting a hook to throw an error, we also had to refactor the expectation like this (it depends on how the error is managed in the hook):

// BEFORE
const { result } = renderHook();
expect(result.error).toEqual(Error('Expected error message'));

// AFTER
expect(() => renderHook()).toThrow(
  'Expected error message'
);
React is now stricter with state updates (especially with DOM events), so we needed to use the act() utility function more than before. Keep in mind, we should import this utility directly from the react package.
When working with events emulation, we had to use the fireEvent helpers:
// BEFORE
input.click();

// AFTER
fireEvent.click(input);
Also, most of the time we had to await the calls to the act() helper function.
In cases where fakeTimers were used, it was necessary to wrap them in act directly.

For example:

// BEFORE
await jest.runAllTimersAsync()

// AFTER
await act(async () => await jest.runAllTimersAsync());
Furthermore, some fakeTimers cases failed if they were subject to React 18's more aggressive state update batching strategy. For some tests, we needed to find the minimum "tick" to achieve a state update, and repeat accordingly.
// BEFORE
it.only('should render 35% after ten seconds', async () => {
  const { getByText } = renderApp(
    <ProjectCreateProgressBar dataset={sampleDatasets.B2B} />
  );
  act(() => {
    jest.advanceTimersByTime(10000);
  });
  expect(getByText(/35%/i)).toBeInTheDocument();
});

// AFTER
it.only('should render 35% after ten seconds', async () => {
  const { getByText } = renderApp(
    <ProjectCreateProgressBar dataset={sampleDatasets.B2B} />
  );
  await act(async () => jest.advanceTimersByTimeAsync(3000));
  await act(async () => jest.advanceTimersByTimeAsync(3000));
  await act(async () => jest.advanceTimersByTimeAsync(3000));
  expect(getByText(/35%/i)).toBeInTheDocument();
});

General code adjustments

We found some issues passing the whole props object to a child in a component due to the key prop, so it's better to destructure that property before passing the props to the child.

This is the warning we saw in the logs:

Warning: A props object containing a "key" prop is being spread into JSX

and here is how we refactored our code:

// Before
<LocalBreadcrumbNode {...props} />

// After
const { key, ...restProps } = props;
<LocalBreadcrumbNode key={key} {...restProps} />

Also, we found some places where we were wrapping a single component with a React.Fragment and now that yields a warning as well:

// BEFORE
const MyComponent = () => (
  <>
    <h1>My title</h1>
  </>
);

// AFTER
const MyComponent = () => (
  <h1>My title</h1>
);

TypeScript type changes

React 19 is stricter with TypeScript types, so we had to adjust some things:

When we were modifying a component using React.cloneElement by adding/removing/updating props, it was necessary to define types for the props expected in the child being cloned.
// BEFORE
type TButtonProps = {
  text: string;
  icon: ReactElement;
};

// AFTER
type TButtonProps = {
  text: string;
  icon: ReactElement<TIconProps>;
};
Using the useRef hook now requires the function to be initialized even when we don't have an initial value.
// BEFORE
const myRef = useRef<string>();

// AFTER
const myRef = useRef<string>(undefined);
On the other hand, the useReducer hook works without the generic params:
// BEFORE
const myReducer = useReducer<TReducerParams>();

// AFTER
const myReducer = useReducer();
The Ref and RefObject types have been replaced by LegacyRef and MutableRefObject.
We had some issues with third-party libraries that didn't have updated types valid for React 19 that were throwing some errors when type checking, specifically regarding the JSX namespace (which is not global anymore in this React version). We decided to enable the skipLibCheck parameter in the tsconfig.json configuration file.

React-intl updates

A combination of the new react-intl major version we're using and the stricter TS types validation from React 19 pushed us to change the way we provided parameters to i18n messages when the value of the parameters were JSX elements.

In this case, we need to provide a key property to the element provided as a parameter:

// BEFORE
<FormattedMessage
  {...messages.myMessage}
  values={{
    name: (
      <Text.Body as="span">{user.name}</Text.Body>
    )
  }}
/>

// AFTER
<FormattedMessage
  {...messages.myMessage}
  values={{
    name: (
      <Text.Body key="username" as="span">{user.name}</Text.Body>
    )
  }}
/>
This also applied to where we were using the useIntl hook (formatMessage() helper function).

React-transition-group updates

The <CSSTransition /> component needs a nodeRef attached (via useRef/createRef from React). And its immediate (and only) child needs the same ref attached but via the regular ref property.
const nodeRef = useRef();

<CSSTransition nodeRef={nodeRef}>
  <div ref={nodeRef}>
  </div>
</CSSTransition>

Formik updates

We use the Formik library in our applications to manage HTML forms, and we found issues in certain scenarios where we were expecting some form state updates to have been consolidated, but they weren't.

Formik holds an internal state in a reducer and tries to trigger React re-renders (by another internal useState state) whenever something changes. The main issue is that React 18 introduced batched state updates. This caused some internal form changes to not be synced with the React state, so the code that was waiting for the new state found the old one.

We found a workaround by forcing a React state synchronization programmatically:

// BEFORE
const handleFormSubmit = (values, formikBag) => {
  saveValues(values);
  formikBag.resetForm();
  validateCleanForm(); // <--- This was not finding the form reset
};

// AFTER
const handleFormSubmit = (values, formikBag) => {
  saveValues(values);
  React.flushSync(() => formikBag.resetForm());
  validateCleanForm(); // <--- Now the form is reset here
};

Custom Views

To allow local development of Custom Views migrated to React 19, you need to turn off Strict Mode.

To turn off Strict Mode, update your Custom View entry point component:

/src/components/entry-point/entry-point.tsxjsx
// BEFORE
<CustomViewShell enableReactStrictMode applicationMessages={loadMessages}>
  <AsyncApplicationRoutes />
</CustomViewShell>

// AFTER
<CustomViewShell applicationMessages={loadMessages}>
  <AsyncApplicationRoutes />
</CustomViewShell>