Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/tame-lands-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"mobx-view-model": minor
---

optimize `withViewModel` HOC with replaced `observer()` wrap HOC to `<Observer>` component use
78 changes: 75 additions & 3 deletions src/react/hoc/with-view-model.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,74 @@ describe('withViewModel', () => {
expect(spyFallbackRender).toHaveBeenCalledTimes(1);
});

test('updates UI when isMounted becomes true', async () => {
class VM extends ViewModelBaseMock {
mount() {
setTimeout(() => {
super.mount();
}, 20);
}
}
const View = ({ model }: ViewModelProps<VM>) => {
return <div data-testid={'view'}>{`hello ${model.id}`}</div>;
};
const Fallback = () => {
return <div data-testid={'fallback'}>fallback</div>;
};
const Component = withViewModel(VM, {
generateId: createIdGenerator(),
fallback: Fallback,
})(View);

render(<Component />);

expect(screen.getByTestId('fallback')).toBeDefined();

await act(async () => {
await sleep(30);
});

expect(screen.getByTestId('view')).toBeDefined();
});

test('updates UI when isMounted becomes false', async () => {
class VM extends ViewModelBaseMock {
triggerUnmount() {
this.unmount();
}
}
const View = ({ model }: ViewModelProps<VM>) => {
return (
<div>
<button
data-testid={'unmount'}
onClick={() => model.triggerUnmount()}
>
unmount
</button>
<div data-testid={'view'}>{`hello ${model.id}`}</div>
</div>
);
};
const Fallback = () => {
return <div data-testid={'fallback'}>fallback</div>;
};
const Component = withViewModel(VM, {
generateId: createIdGenerator(),
fallback: Fallback,
})(View);

await act(async () => render(<Component />));

expect(screen.getByTestId('view')).toBeDefined();

await act(async () => {
fireEvent.click(screen.getByTestId('unmount'));
});

expect(screen.getByTestId('fallback')).toBeDefined();
});

test('renders fallback before render REAL COMPONENT (times)', async () => {
class VM extends ViewModelBaseMock {}
const View = ({ model }: ViewModelProps<VM>) => {
Expand Down Expand Up @@ -249,7 +317,7 @@ describe('withViewModel', () => {
expect(screen.getByText('second-VM_1')).toBeDefined();
});

test('withViewModel wrapper should by only mounted (renders 2 times)', () => {
test('withViewModel wrapper should by only mounted (renders 1 time)', () => {
class VM extends ViewModelBaseMock {}
const View = vi.fn(({ model }: ViewModelProps<VM>) => {
return <div>{`hello ${model.id}`}</div>;
Expand All @@ -263,7 +331,7 @@ describe('withViewModel', () => {
})(View);

render(<Component />);
expect(useHookSpy).toHaveBeenCalledTimes(2);
expect(useHookSpy).toHaveBeenCalledTimes(1);
});

describe('payload manipulations', () => {
Expand Down Expand Up @@ -746,7 +814,11 @@ describe('withViewModel', () => {

await sleep(200);

expect(setPayloadSpy).toHaveBeenCalledTimes(2);
expect(setPayloadSpy).toHaveBeenCalledTimes(1);
expect(setPayloadSpy.mock.calls[0]?.[0]).toMatchObject({
techreviewId: '1',
selectedCompIds: ['1', '2', '3'],
});
});
});

Expand Down
45 changes: 25 additions & 20 deletions src/react/hoc/with-view-model.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { observer } from 'mobx-react-lite';
import { Observer, observer } from 'mobx-react-lite';
import { forwardRef, useContext } from 'react';
import type {
AnyObject,
Expand Down Expand Up @@ -373,24 +373,31 @@ const withViewModelWrapper = (
props: componentProps,
}) as unknown as AnyViewModel | AnyViewModelSimple;

const isRenderAllowedByStore =
!viewModels || viewModels.isAbleToRenderView(model.id);

// This condition is works for AnyViewModelSimple too
// All other variants will be bad for performance
const isRenderAllowed =
isRenderAllowedByStore && (model as AnyViewModel).isMounted !== false;

if (isRenderAllowed) {
return (
<ActiveViewModelProvider value={model}>
{Component && <Component {...componentProps} model={model} />}
</ActiveViewModelProvider>
);
}

return (
FallbackComponent && <FallbackComponent {...allProps} payload={payload} />
<Observer>
{() => {
const isRenderAllowedByStore =
!viewModels || viewModels.isAbleToRenderView(model.id);

// This condition is works for AnyViewModelSimple too
// All other variants will be bad for performance
const isRenderAllowed =
isRenderAllowedByStore &&
(model as AnyViewModel).isMounted !== false;

if (isRenderAllowed) {
return (
<ActiveViewModelProvider value={model}>
{Component && <Component {...componentProps} model={model} />}
</ActiveViewModelProvider>
);
}

return FallbackComponent ? (
<FallbackComponent {...allProps} payload={payload} />
) : null;
}}
</Observer>
);
};

Expand All @@ -400,8 +407,6 @@ const withViewModelWrapper = (
ConnectedViewModel = forwardRef(ConnectedViewModel) as any;
}

ConnectedViewModel = observer(ConnectedViewModel);

if (process.env.NODE_ENV !== 'production') {
(ConnectedViewModel as React.ComponentType).displayName =
`ConnectedViewModel(${VM.name}->Component)`;
Expand Down