Skip to content

Commit c0d9a4e

Browse files
committed
BREAKING_CHANGE: View components will be rendered only if VM.isMounted truthy (default .mount() behaviour);BREAKING_CHANGE: remove 'instances' property from ViewModelCreateConfig, ViewModelGenerateIdConfig;
1 parent 0cfcaa7 commit c0d9a4e

File tree

3 files changed

+105
-42
lines changed

3 files changed

+105
-42
lines changed

src/hoc/with-view-model.test.tsx

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,84 @@ const createIdGenerator = (prefix?: string) => {
1717
};
1818

1919
describe('withViewModel', () => {
20-
test('renders', () => {
21-
class VM extends ViewModelMock {}
20+
test('renders', async () => {
21+
class VM extends ViewModelMock {
22+
mount() {
23+
super.mount();
24+
}
25+
}
2226
const View = ({ model }: ViewModelProps<VM>) => {
23-
return <div>{`hello ${model.id}`}</div>;
27+
return <div data-testid={'view'}>{`hello ${model.id}`}</div>;
2428
};
2529
const Component = withViewModel(VM, { generateId: createIdGenerator() })(
2630
View,
2731
);
2832

29-
render(<Component />);
33+
await act(async () => render(<Component />));
3034
expect(screen.getByText('hello VM_0')).toBeDefined();
3135
});
3236

37+
test('renders fallback', async () => {
38+
class VM extends ViewModelMock {
39+
// eslint-disable-next-line sonarjs/no-empty-function
40+
mount() {}
41+
}
42+
const View = ({ model }: ViewModelProps<VM>) => {
43+
return <div data-testid={'view'}>{`hello ${model.id}`}</div>;
44+
};
45+
const Component = withViewModel(VM, {
46+
generateId: createIdGenerator(),
47+
fallback: () => {
48+
return 'fallback';
49+
},
50+
})(View);
51+
52+
const { container } = await act(async () => render(<Component />));
53+
expect(container).toMatchInlineSnapshot(`
54+
<div>
55+
fallback
56+
</div>
57+
`);
58+
});
59+
60+
test('renders fallback (times)', async () => {
61+
class VM extends ViewModelMock {
62+
// eslint-disable-next-line sonarjs/no-empty-function
63+
mount() {}
64+
}
65+
const View = ({ model }: ViewModelProps<VM>) => {
66+
return <div data-testid={'view'}>{`hello ${model.id}`}</div>;
67+
};
68+
69+
const spyFallbackRender = vi.fn(() => 'fallback');
70+
71+
const Component = withViewModel(VM, {
72+
generateId: createIdGenerator(),
73+
fallback: spyFallbackRender,
74+
})(View);
75+
76+
await act(async () => render(<Component />));
77+
expect(spyFallbackRender).toHaveBeenCalledTimes(1);
78+
});
79+
80+
test('renders fallback before render REAL COMPONENT (times)', async () => {
81+
class VM extends ViewModelMock {}
82+
const View = ({ model }: ViewModelProps<VM>) => {
83+
return <div data-testid={'view'}>{`hello ${model.id}`}</div>;
84+
};
85+
86+
const spyFallbackRender = vi.fn(() => 'fallback');
87+
88+
const Component = withViewModel(VM, {
89+
generateId: createIdGenerator(),
90+
fallback: spyFallbackRender,
91+
})(View);
92+
93+
await act(async () => render(<Component />));
94+
95+
expect(spyFallbackRender).toHaveBeenCalledTimes(1);
96+
});
97+
3398
test('renders nesting', () => {
3499
const Component1 = withViewModel(ViewModelMock)(({
35100
children,
@@ -126,7 +191,7 @@ describe('withViewModel', () => {
126191
expect(View).toHaveBeenCalledTimes(1);
127192
});
128193

129-
test('withViewModel wrapper should by only mounted (renders only 1 time)', () => {
194+
test('withViewModel wrapper should by only mounted (renders 2 times)', () => {
130195
class VM extends ViewModelMock {}
131196
const View = vi.fn(({ model }: ViewModelProps<VM>) => {
132197
return <div>{`hello ${model.id}`}</div>;
@@ -140,7 +205,7 @@ describe('withViewModel', () => {
140205
})(View);
141206

142207
render(<Component />);
143-
expect(useHookSpy).toHaveBeenCalledTimes(1);
208+
expect(useHookSpy).toHaveBeenCalledTimes(2);
144209
});
145210

146211
test('View should be updated when payload is changed', async () => {
@@ -344,7 +409,7 @@ describe('withViewModel', () => {
344409
);
345410

346411
expect(viewModels).toBeDefined();
347-
expect(vmStore.spies.get).toHaveBeenCalledTimes(0);
412+
expect(vmStore.spies.get).toHaveBeenCalledTimes(3);
348413
expect(vmStore._instanceAttachedCount.size).toBe(1);
349414
expect(vmStore._unmountingViews.size).toBe(0);
350415
expect(vmStore.mountedViewsCount).toBe(1);
@@ -418,7 +483,7 @@ describe('withViewModel', () => {
418483
expect(container.firstChild).toMatchFileSnapshot(
419484
`../../tests/snapshots/hoc/with-view-model/view-model-store/${task.name}.html`,
420485
);
421-
expect(vmStore.spies.get).toHaveBeenCalledTimes(0);
486+
expect(vmStore.spies.get).toHaveBeenCalledTimes(15);
422487
expect(vmStore._instanceAttachedCount.size).toBe(3);
423488
expect(vmStore._unmountingViews.size).toBe(0);
424489
expect(vmStore.mountedViewsCount).toBe(3);

src/hoc/with-view-model.tsx

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import {
44
ComponentType,
55
ReactNode,
66
useContext,
7-
useEffect,
87
useLayoutEffect,
8+
useMemo,
99
useRef,
1010
} from 'react';
1111

1212
import { ActiveViewModelContext, ViewModelsContext } from '../contexts';
1313
import { generateVMId } from '../utils';
1414
import { AnyObject, Class, EmptyObject, Maybe } from '../utils/types';
15-
import { AnyViewModel, ViewModel, ViewModelCreateConfig } from '../view-model';
15+
import { AnyViewModel, ViewModelCreateConfig } from '../view-model';
1616

1717
declare const process: { env: { NODE_ENV?: string } };
1818

@@ -25,38 +25,38 @@ export type ViewModelInputProps<VM extends AnyViewModel> =
2525

2626
export type ViewModelHocConfig<VM extends AnyViewModel> = {
2727
/**
28-
* Уникальный идентификатор вьюшки
28+
* Unique identifier for the view
2929
*/
3030
id?: Maybe<string>;
3131

3232
/**
33-
* Функция генератор идентификатор для вью модели
33+
* Function to generate an identifier for the view model
3434
*/
3535
generateId?: (ctx: AnyObject) => string;
3636

3737
/**
38-
* Компонент, который будет отрисован в случае если инциализация вью модели происходит слишком долго
38+
* Component to render if the view model initialization takes too long
3939
*/
4040
fallback?: ComponentType;
4141

4242
/**
43-
* Доп. данные, которые могут быть полезны при создании VM
43+
* Additional data that may be useful when creating the VM
4444
*/
4545
ctx?: AnyObject;
4646

4747
/**
48-
* Функция, в которой можно вызывать доп. реакт хуки в результирующем компоненте
48+
* Function to invoke additional React hooks in the resulting component
4949
*/
5050
reactHooks?: (allProps: any) => void;
5151

5252
/**
53-
* Функция, которая должна возвращать payload для VM
54-
* по умолчанию это - (props) => props.payload
53+
* Function that should return the payload for the VM
54+
* by default, it is - (props) => props.payload
5555
*/
5656
getPayload?: (allProps: any) => any;
5757

5858
/**
59-
* Функция создания экземпляра класса VM
59+
* Function to create an instance of the VM class
6060
*/
6161
factory?: (config: ViewModelCreateConfig<VM>) => VM;
6262
};
@@ -85,8 +85,6 @@ export function withViewModel(
8585
ctx.generateId = config?.generateId;
8686

8787
return (Component: ComponentType<any>) => {
88-
const instances = new Map<string, AnyViewModel>();
89-
9088
const ConnectedViewModel = observer((allProps: any) => {
9189
const { payload: rawPayload, ...componentProps } = allProps;
9290

@@ -106,15 +104,20 @@ export function withViewModel(
106104
VM: Model,
107105
parentViewModelId: parentViewModel?.id,
108106
fallback: config?.fallback,
109-
instances,
110107
}) ??
111108
config?.id ??
112109
generateVMId(ctx);
113110
}
114111

115112
const id = idRef.current;
116113

117-
if (!instances.has(id)) {
114+
const instanceFromStore = viewModels ? viewModels.get(id) : null;
115+
116+
const instance = useMemo(() => {
117+
if (instanceFromStore) {
118+
return instanceFromStore;
119+
}
120+
118121
const configCreate: ViewModelCreateConfig<any> = {
119122
id,
120123
parentViewModelId: parentViewModel?.id,
@@ -123,7 +126,6 @@ export function withViewModel(
123126
viewModels,
124127
parentViewModel,
125128
fallback: config?.fallback,
126-
instances,
127129
ctx,
128130
component: ConnectedViewModel,
129131
componentProps,
@@ -136,37 +138,36 @@ export function withViewModel(
136138
viewModels?.createViewModel<any>(configCreate) ??
137139
new Model(configCreate);
138140

139-
instances.set(id, instance);
140-
}
141+
return instance;
142+
}, [instanceFromStore]);
141143

142-
const instance: ViewModel = instances.get(id)!;
144+
const isRenderAllowedByStore =
145+
!viewModels || viewModels.isAbleToRenderView(id);
146+
const isRenderAllowedLocally = !!instance?.isMounted;
147+
const isRenderAllowed = isRenderAllowedByStore && isRenderAllowedLocally;
143148

144-
useEffect(() => {
149+
useLayoutEffect(() => {
145150
if (viewModels) {
146151
viewModels.attach(instance);
152+
return () => {
153+
viewModels.detach(id);
154+
};
147155
} else {
148156
instance.mount();
149-
}
150-
151-
return () => {
152-
if (viewModels) {
153-
viewModels.detach(id);
154-
} else {
157+
return () => {
155158
instance.unmount();
156-
}
157-
instances.delete(id);
158-
};
159-
// eslint-disable-next-line react-hooks/exhaustive-deps
159+
};
160+
}
160161
}, []);
161162

162163
useLayoutEffect(() => {
163-
instance.setPayload(payload);
164+
instance?.setPayload(payload);
164165
// eslint-disable-next-line react-hooks/exhaustive-deps
165166
}, [payload]);
166167

167168
config?.reactHooks?.(allProps);
168169

169-
if ((!viewModels || viewModels.isAbleToRenderView(id)) && instance) {
170+
if (isRenderAllowed) {
170171
return (
171172
<ActiveViewModelContext.Provider value={instance}>
172173
<Component {...(componentProps as any)} model={instance} />

src/view-model/view-model.store.types.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { ComponentType } from 'react';
33
import { ComponentWithViewModel } from '../hoc';
44
import { AnyObject, Class, Maybe } from '../utils/types';
55

6-
import { ViewModel } from './view-model';
76
import { AnyViewModel, ViewModelParams } from './view-model.types';
87

98
export interface ViewModelGenerateIdConfig<VM extends AnyViewModel> {
@@ -12,14 +11,12 @@ export interface ViewModelGenerateIdConfig<VM extends AnyViewModel> {
1211
ctx: AnyObject;
1312
parentViewModelId: string | null;
1413
fallback?: ComponentType;
15-
instances: Map<string, ViewModel>;
1614
}
1715

1816
export interface ViewModelCreateConfig<VM extends AnyViewModel>
1917
extends ViewModelParams<VM['payload'], VM['parentViewModel']> {
2018
VM: Class<VM>;
2119
fallback?: ComponentType;
22-
instances: Map<string, ViewModel>;
2320
component: ComponentWithViewModel<AnyViewModel, any>;
2421
componentProps: AnyObject;
2522
}

0 commit comments

Comments
 (0)