Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
106 changes: 106 additions & 0 deletions packages/bridge/bridge-react/RERENDER_EXAMPLE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Bridge React Rerender Functionality

This document demonstrates the new rerender functionality that solves the performance issue described in [GitHub Issue #4171](https://github.com/module-federation/core/issues/4171).

## Problem

Previously, when a host component rerendered and passed new props to a remote component, the entire remote app would be recreated instead of just rerendering, causing performance issues.

## Solution

The new rerender functionality provides two approaches:

### 1. Automatic Efficient Rerender (Default)

By default, the bridge now uses React state management to efficiently update props without recreating the React root:

```tsx
// Remote App (no changes needed for basic optimization)
export default createBridgeComponent({
rootComponent: App,
});
```

```tsx
// Host App
function HostApp() {
const [count, setCount] = React.useState(0);

return (
<>
<button onClick={() => setCount(s => s + 1)}>Count {count}</button>
<Remote1App props={{ message: 'Hello', count }} />
</>
);
}
```

**Result**: Clicking the button now efficiently updates the remote component without recreating the entire React tree.

### 2. Custom Rerender Function

For advanced use cases, you can provide a custom rerender function:

```tsx
// Remote App with custom rerender logic
export default createBridgeComponent({
rootComponent: App,
rerender: (props) => {
// Custom rerender logic
console.log('Efficiently updating with new props:', props);

// You can implement custom state management here
// For example, updating a global store or context
updateGlobalStore(props);
},
});
```

## Performance Benefits

- **Before**: Every prop change triggered `root.render()`, recreating the entire component tree
- **After**: Prop changes use React state updates or custom rerender logic, maintaining component state and improving performance

## Backward Compatibility

The implementation is fully backward compatible:
- Existing code continues to work without changes
- The automatic optimization is applied by default
- Custom rerender functions are optional

## API Reference

### ProviderFnParams Interface

```tsx
interface ProviderFnParams<T> {
rootComponent: React.ComponentType<T>;
render?: (App: React.ReactElement, id?: HTMLElement | string) => RootType | Promise<RootType>;
createRoot?: (container: Element | DocumentFragment, options?: CreateRootOptions) => Root;
defaultRootOptions?: CreateRootOptions;

// New: Optional custom rerender function
rerender?: (props: T) => void;
}
```

### BridgeComponentInstance Interface

```tsx
interface BridgeComponentInstance {
render: (info: RenderParams) => Promise<void>;
destroy: (info: DestroyParams) => void;

// New: Optional rerender method
rerender?: (props: any) => void;
}
```

## Implementation Details

1. **State Management**: The bridge now uses a `StatefulBridgeWrapper` component with React state to manage props
2. **Efficient Updates**: When props change, the system uses `setState` instead of `root.render()`
3. **Custom Logic**: If a `rerender` function is provided, it's called instead of the default state update
4. **Fallback**: If rerender is not available, the system falls back to the original behavior for compatibility

This solution addresses the core performance issue while maintaining full backward compatibility and providing flexibility for advanced use cases.
228 changes: 228 additions & 0 deletions packages/bridge/bridge-react/__tests__/bridge.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,232 @@ describe('bridge', () => {
{ timeout: 2000 },
);
});

it('createBridgeComponent with custom rerender function', async () => {
const rerenderMock = jest.fn();
let componentProps: any = { count: 0 };

function Component(props: any) {
componentProps = props;
return <div>count: {props.count}</div>;
}

const lifeCycle = createBridgeComponent({
rootComponent: Component,
rerender: rerenderMock,
})();

// Initial render
await lifeCycle.render({
dom: containerInfo?.container,
count: 1,
});

await waitFor(
() => {
expect(document.querySelector('#container')?.innerHTML).toContain(
'count: 1',
);
},
{ timeout: 2000 },
);

// Test rerender functionality
if (lifeCycle.rerender) {
lifeCycle.rerender({ count: 2 });
expect(rerenderMock).toHaveBeenCalledWith({ count: 2 });
}
});

it('createRemoteAppComponent prop updates use efficient rerender', async () => {
const renderMock = jest.fn();
const rerenderMock = jest.fn();
let renderCount = 0;

function Component(props: any) {
renderCount++;
return <div>count: {props.count}, renders: {renderCount}</div>;
}

const BridgeComponent = createBridgeComponent({
rootComponent: Component,
rerender: rerenderMock,
createRoot: () => {
return {
render: renderMock,
unmount: jest.fn(),
};
},
});

const RemoteComponent = createRemoteAppComponent({
loader: async () => {
return {
default: BridgeComponent,
};
},
fallback: () => <div></div>,
loading: <div>loading</div>,
});

const TestWrapper = () => {
const [count, setCount] = React.useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<RemoteComponent props={{ count }} />
</div>
);
};

const { container } = render(<TestWrapper />);

// Wait for initial load
await waitFor(
() => {
expect(getHtml(container)).toMatch('count: 0');
},
{ timeout: 2000 },
);

// Simulate prop update
const button = container.querySelector('button');
if (button) {
fireEvent.click(button);

await waitFor(
() => {
// Should use rerender instead of full render
expect(rerenderMock).toHaveBeenCalled();
},
{ timeout: 2000 },
);
}
});

it('createRemoteAppComponent falls back to full render when rerender not available', async () => {
const renderMock = jest.fn();
let renderCount = 0;

function Component(props: any) {
renderCount++;
return <div>count: {props.count}, renders: {renderCount}</div>;
}

const BridgeComponent = createBridgeComponent({
rootComponent: Component,
// No rerender function provided
createRoot: () => {
return {
render: renderMock,
unmount: jest.fn(),
};
},
});

const RemoteComponent = createRemoteAppComponent({
loader: async () => {
return {
default: BridgeComponent,
};
},
fallback: () => <div></div>,
loading: <div>loading</div>,
});

const TestWrapper = () => {
const [count, setCount] = React.useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<RemoteComponent props={{ count }} />
</div>
);
};

const { container } = render(<TestWrapper />);

// Wait for initial load
await waitFor(
() => {
expect(getHtml(container)).toMatch('count: 0');
},
{ timeout: 2000 },
);

const initialRenderCount = renderMock.mock.calls.length;

// Simulate prop update
const button = container.querySelector('button');
if (button) {
fireEvent.click(button);

await waitFor(
() => {
// Should call render again for fallback
expect(renderMock.mock.calls.length).toBeGreaterThan(initialRenderCount);
},
{ timeout: 2000 },
);
}
});

it('createBridgeComponent state management prevents unnecessary root recreation', async () => {
let stateUpdateCount = 0;
const originalSetState = React.useState;

// Mock useState to track state updates
jest.spyOn(React, 'useState').mockImplementation((initial) => {
const [state, setState] = originalSetState(initial);
return [state, (newState: any) => {
stateUpdateCount++;
setState(newState);
}];
});

function Component(props: any) {
return <div>message: {props.message}</div>;
}

const lifeCycle = createBridgeComponent({
rootComponent: Component,
})();

// Initial render
await lifeCycle.render({
dom: containerInfo?.container,
message: 'hello',
});

await waitFor(
() => {
expect(document.querySelector('#container')?.innerHTML).toContain(
'message: hello',
);
},
{ timeout: 2000 },
);

const initialStateUpdateCount = stateUpdateCount;

// Second render with different props
await lifeCycle.render({
dom: containerInfo?.container,
message: 'world',
});

await waitFor(
() => {
expect(document.querySelector('#container')?.innerHTML).toContain(
'message: world',
);
// Should use state update instead of recreating root
expect(stateUpdateCount).toBeGreaterThan(initialStateUpdateCount);
},
{ timeout: 2000 },
);

// Restore original useState
(React.useState as jest.Mock).mockRestore();
});
});
Loading
Loading