@reearth/core 0.0.7-alpha.66 → 0.0.7-alpha.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reearth/core",
3
- "version": "0.0.7-alpha.66",
3
+ "version": "0.0.7-alpha.67",
4
4
  "author": "Re:Earth contributors <community@reearth.io>",
5
5
  "license": "Apache-2.0",
6
6
  "description": "A library that abstracts a map engine as one common API.",
@@ -1,10 +1,44 @@
1
1
  import { renderHook } from "@testing-library/react";
2
2
  import { UrlTemplateImageryProvider } from "cesium";
3
- import { expect, test, vi } from "vitest";
3
+ import { expect, test, vi, beforeEach } from "vitest";
4
4
 
5
5
  import type { CustomProviderConfig } from "../../../Map/types/customProvider";
6
6
 
7
- import { type Tile, useImageryProviders } from "./Imagery";
7
+ import ImageryLayers, { type Tile, useImageryProviders } from "./Imagery";
8
+
9
+ // Mock Cesium scene and imageryLayerCollection for ImageryLayers component tests
10
+ const mockAdd = vi.fn();
11
+ const mockRemove = vi.fn();
12
+ const mockContains = vi.fn(() => true);
13
+ const mockIndexOf = vi.fn(() => 0);
14
+ const mockRequestRender = vi.fn();
15
+
16
+ const mockImageryLayerCollection = {
17
+ add: mockAdd,
18
+ remove: mockRemove,
19
+ contains: mockContains,
20
+ indexOf: mockIndexOf,
21
+ };
22
+
23
+ const mockScene = {
24
+ requestRender: mockRequestRender,
25
+ isDestroyed: () => false,
26
+ };
27
+
28
+ vi.mock("resium", () => ({
29
+ useCesium: () => ({
30
+ imageryLayerCollection: mockImageryLayerCollection,
31
+ scene: mockScene,
32
+ }),
33
+ }));
34
+
35
+ beforeEach(() => {
36
+ mockAdd.mockClear();
37
+ mockRemove.mockClear();
38
+ mockContains.mockClear();
39
+ mockIndexOf.mockClear();
40
+ mockRequestRender.mockClear();
41
+ });
8
42
 
9
43
  test("useImageryProviders", () => {
10
44
  const provider = vi.fn(({ url }: { url?: string } = {}): any => ({ hoge: url }));
@@ -153,3 +187,58 @@ test("useImageryProviders", () => {
153
187
  typedRerender({ tiles: [] });
154
188
  expect(result.current.providers).toEqual({});
155
189
  });
190
+
191
+ test("ImageryLayers should not re-render when tiles array reference changes but content is the same", async () => {
192
+ const tiles: Tile[] = [{ id: "1", type: "open_street_map", opacity: 0.8 }];
193
+
194
+ const { rerender } = renderHook(
195
+ ({ tiles }: { tiles: Tile[] }) => {
196
+ return ImageryLayers({
197
+ tiles,
198
+ cesiumIonAccessToken: undefined,
199
+ customProvider: undefined,
200
+ onTilesChange: undefined,
201
+ });
202
+ },
203
+ {
204
+ initialProps: { tiles },
205
+ },
206
+ );
207
+
208
+ // Wait for initial render to complete
209
+ await new Promise(resolve => setTimeout(resolve, 0));
210
+
211
+ // Clear mock calls from initial render
212
+ mockAdd.mockClear();
213
+ mockRemove.mockClear();
214
+
215
+ // Re-render with a NEW tiles array reference but SAME content
216
+ const newTilesArraySameContent: Tile[] = [{ id: "1", type: "open_street_map", opacity: 0.8 }];
217
+
218
+ rerender({ tiles: newTilesArraySameContent });
219
+
220
+ // Wait for any effects to run
221
+ await new Promise(resolve => setTimeout(resolve, 0));
222
+
223
+ // Effect should NOT have run again - no layers should be removed or added
224
+ expect(mockRemove).not.toHaveBeenCalled();
225
+ expect(mockAdd).not.toHaveBeenCalled();
226
+
227
+ // Clear mocks
228
+ mockAdd.mockClear();
229
+ mockRemove.mockClear();
230
+
231
+ // Re-render with DIFFERENT content (changed opacity)
232
+ const differentTiles: Tile[] = [
233
+ { id: "1", type: "open_street_map", opacity: 0.5 }, // opacity changed
234
+ ];
235
+
236
+ rerender({ tiles: differentTiles });
237
+
238
+ // Wait for effects to run
239
+ await new Promise(resolve => setTimeout(resolve, 0));
240
+
241
+ // Effect SHOULD run - old layers removed and new ones added
242
+ expect(mockRemove).toHaveBeenCalled();
243
+ expect(mockAdd).toHaveBeenCalled();
244
+ });
@@ -53,8 +53,23 @@ export default function ImageryLayers({
53
53
  }: Props) {
54
54
  const { imageryLayerCollection, scene } = useCesium();
55
55
 
56
+ // Create a stable tiles reference that only changes when the content actually changes
57
+ // Normalize `undefined` to a stable empty array so `useImageryProviders` doesn't allocate a new default `[]` each render.
58
+ const emptyTiles = useMemo<Tile[]>(() => [], []);
59
+ const tilesValue = tiles ?? emptyTiles;
60
+
61
+ const prevTilesRef = useRef<Tile[]>(tilesValue);
62
+ const stableTiles = useMemo(() => {
63
+ if (!isEqual(prevTilesRef.current, tilesValue)) {
64
+ prevTilesRef.current = tilesValue;
65
+ }
66
+ return prevTilesRef.current;
67
+ }, [tilesValue]);
68
+
69
+ // Pass stableTiles to useImageryProviders to prevent providers from being recreated
70
+ // when tiles reference changes but content is the same
56
71
  const { providers } = useImageryProviders({
57
- tiles,
72
+ tiles: stableTiles,
58
73
  cesiumIonAccessToken,
59
74
  customProvider,
60
75
  presets: tilePresets,
@@ -66,7 +81,9 @@ export default function ImageryLayers({
66
81
  let cancelled = false;
67
82
  const addedLayers: CesiumImageryLayer[] = [];
68
83
  // Track layers by their intended index to maintain order with async loading
69
- const layersByIndex: (CesiumImageryLayer | null)[] = new Array(tiles?.length || 0).fill(null);
84
+ const layersByIndex: (CesiumImageryLayer | null)[] = new Array(stableTiles?.length || 0).fill(
85
+ null,
86
+ );
70
87
 
71
88
  const reorderLayers = () => {
72
89
  if (cancelled || scene.isDestroyed()) return;
@@ -91,7 +108,7 @@ export default function ImageryLayers({
91
108
  scene.requestRender();
92
109
  };
93
110
 
94
- tiles?.forEach(({ id, zoomLevel, opacity, heatmap }, i) => {
111
+ stableTiles?.forEach(({ id, zoomLevel, opacity, heatmap }, i) => {
95
112
  const providerOrPromise = providers[id]?.[3];
96
113
  if (!providerOrPromise) return;
97
114
 
@@ -117,7 +134,9 @@ export default function ImageryLayers({
117
134
  };
118
135
 
119
136
  if (providerOrPromise instanceof Promise) {
120
- providerOrPromise.then(doAdd).catch(err => console.error("Failed to load imagery provider:", err));
137
+ providerOrPromise
138
+ .then(doAdd)
139
+ .catch(err => console.error("Failed to load imagery provider:", err));
121
140
  } else {
122
141
  doAdd(providerOrPromise);
123
142
  }
@@ -134,7 +153,9 @@ export default function ImageryLayers({
134
153
  }
135
154
  }
136
155
  };
137
- }, [providers, tiles, imageryLayerCollection, scene, onTilesChange]);
156
+ // Note: Using `stableTiles` to prevent re-renders when tiles reference changes but content is identical.
157
+ // This also stabilizes `providers` since it depends on tiles in useImageryProviders.
158
+ }, [providers, stableTiles, imageryLayerCollection, scene, onTilesChange]);
138
159
 
139
160
  return null;
140
161
  }