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-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.
@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/react
package - 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
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>