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.0downshift: 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-dndwas replaced with@hello-pangea/dndreact-sortable-hocwas 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.
@testing-library/react-hooks dependency anymore.renderHook utility function directly from @testing-library/react package.This utility can be referenced in two ways:
- From the 
@testing-library/reactpackage - From the 
@commercetools-frontend/application-shell/test-utilspackage- 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
 
 
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'
);
act() utility function more than before. Keep in mind, we should import this utility directly from the react package.fireEvent helpers:// BEFORE
input.click();
// AFTER
fireEvent.click(input);
act() helper function.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());
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:
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>;
};
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);
useReducer hook works without the generic params:// BEFORE
const myReducer = useReducer<TReducerParams>();
// AFTER
const myReducer = useReducer();
Ref and RefObject types have been replaced by LegacyRef and MutableRefObject.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>
    )
  }}
/>
useIntl hook (formatMessage() helper function).React-transition-group updates
<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:
// BEFORE
<CustomViewShell enableReactStrictMode applicationMessages={loadMessages}>
  <AsyncApplicationRoutes />
</CustomViewShell>
// AFTER
<CustomViewShell applicationMessages={loadMessages}>
  <AsyncApplicationRoutes />
</CustomViewShell>