Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,13 @@
"react-redux": "^7.1.1 || ^8.1.1",
"react-router-dom": "^6.0.0",
"redux": "^4.0.4"
},
"peerDependenciesMeta": {
"redux": {
"optional": true
},
"react-redux": {
"optional": true
}
}
}
38 changes: 16 additions & 22 deletions src/react/AppProvider.test.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { createStore } from 'redux';
import { render, waitFor } from '@testing-library/react';
import { render } from '@testing-library/react';
import AppProvider from './AppProvider';
import { initialize } from '../initialize';

Expand Down Expand Up @@ -48,7 +48,7 @@ describe('AppProvider', () => {
});
});

it('should render its children with a router', async () => {
it('should render its children with a router', () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[inform] Needing to make these tests async was a clue that the previous approach with React.lazy was technically a breaking change for consuming MFEs' tests.

Also, note, I believe using React.lazy here would have prevented the component from being able to get rendered with SSR, as React.lazy depends on dynamic runtime imports. Using a dynamic import within a useEffect appears to be compatible with SSR.

const component = (
<AppProvider store={createStore(state => state)}>
<div className="child">Child One</div>
Expand All @@ -57,18 +57,16 @@ describe('AppProvider', () => {
);

const wrapper = render(component);
await waitFor(() => {
const list = wrapper.container.querySelectorAll('div.child');
expect(list.length).toEqual(2);
expect(list[0].textContent).toEqual('Child One');
expect(list[1].textContent).toEqual('Child Two');
});
const list = wrapper.container.querySelectorAll('div.child');
expect(list.length).toEqual(2);
expect(list[0].textContent).toEqual('Child One');
expect(list[1].textContent).toEqual('Child Two');
expect(wrapper.getByTestId('browser-router')).toBeInTheDocument();
const reduxProvider = wrapper.getByTestId('redux-provider');
expect(reduxProvider).toBeInTheDocument();
});

it('should render its children without a router', async () => {
it('should render its children without a router', () => {
const component = (
<AppProvider store={createStore(state => state)} wrapWithRouter={false}>
<div className="child">Child One</div>
Expand All @@ -77,18 +75,16 @@ describe('AppProvider', () => {
);

const wrapper = render(component);
await waitFor(() => {
const list = wrapper.container.querySelectorAll('div.child');
expect(list.length).toEqual(2);
expect(list[0].textContent).toEqual('Child One');
expect(list[1].textContent).toEqual('Child Two');
});
const list = wrapper.container.querySelectorAll('div.child');
expect(list.length).toEqual(2);
expect(list[0].textContent).toEqual('Child One');
expect(list[1].textContent).toEqual('Child Two');
expect(wrapper.queryByTestId('browser-router')).not.toBeInTheDocument();
const reduxProvider = wrapper.getByTestId('redux-provider');
expect(reduxProvider).toBeInTheDocument();
});

it('should skip redux Provider if not given a store', async () => {
it('should skip redux Provider if not given a store', () => {
const component = (
<AppProvider>
<div className="child">Child One</div>
Expand All @@ -97,12 +93,10 @@ describe('AppProvider', () => {
);

const wrapper = render(component);
await waitFor(() => {
const list = wrapper.container.querySelectorAll('div.child');
expect(list.length).toEqual(2);
expect(list[0].textContent).toEqual('Child One');
expect(list[1].textContent).toEqual('Child Two');
});
const list = wrapper.container.querySelectorAll('div.child');
expect(list.length).toEqual(2);
expect(list[0].textContent).toEqual('Child One');
expect(list[1].textContent).toEqual('Child Two');

const reduxProvider = wrapper.queryByTestId('redux-provider');
expect(reduxProvider).not.toBeInTheDocument();
Expand Down
42 changes: 40 additions & 2 deletions src/react/OptionalReduxProvider.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,54 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Provider } from 'react-redux';

function useProvider(store) {
const [Provider, setProvider] = useState(null);
useEffect(() => {
if (!store) {
setProvider(() => ({ children: c }) => c);
return;
}
if (process.env.NODE_ENV === 'test') {
// In test environments, load react-redux synchronously to avoid async state updates.
try {
// eslint-disable-next-line global-require
const module = require('react-redux');
setProvider(() => module.Provider);
} catch {
setProvider(() => ({ children: c }) => c);
}
} else {

Check warning on line 20 in src/react/OptionalReduxProvider.jsx

View check run for this annotation

Codecov / codecov/patch

src/react/OptionalReduxProvider.jsx#L20

Added line #L20 was not covered by tests
// In production, load react-redux dynamically.
import('react-redux')
.then((module) => {
setProvider(() => module.Provider);

Check warning on line 24 in src/react/OptionalReduxProvider.jsx

View check run for this annotation

Codecov / codecov/patch

src/react/OptionalReduxProvider.jsx#L22-L24

Added lines #L22 - L24 were not covered by tests
})
.catch(() => {
setProvider(() => ({ children: c }) => c);

Check warning on line 27 in src/react/OptionalReduxProvider.jsx

View check run for this annotation

Codecov / codecov/patch

src/react/OptionalReduxProvider.jsx#L26-L27

Added lines #L26 - L27 were not covered by tests
});
}
}, [store]);
return Provider;
}

/**
* @memberof module:React
* @param {Object} props
*/
export default function OptionalReduxProvider({ store = null, children }) {
const Provider = useProvider(store);

// If the Provider is not loaded yet, we return null to avoid rendering issues
if (!Provider) {
return null;
}

// If the store is null, we return the children directly as no Provider is needed
if (store === null) {
return children;
}

// If the Provider is loaded and the store is not null, we render the Provider with the children
return (
<Provider store={store}>
<div data-testid="redux-provider">
Expand Down
23 changes: 23 additions & 0 deletions src/react/OptionalReduxProvider.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import OptionalReduxProvider from './OptionalReduxProvider'; // Adjust the import path as needed

describe('OptionalReduxProvider', () => {
it('should handle error when react-redux import fails', async () => {
// Simulate the failed import of 'react-redux'
jest.mock('react-redux', () => {
throw new Error('Failed to load react-redux');
});

const mockStore = {}; // Mock store object
render(
<OptionalReduxProvider store={mockStore}>
<span>Test Children</span>
</OptionalReduxProvider>,
);

// Check that the children are still rendered even when react-redux fails to load
const childrenElement = await screen.findByText('Test Children');
expect(childrenElement).toBeInTheDocument();
});
});