Skip to content

Commit 89687de

Browse files
authored
Add support for auto-discovering initial content (#524)
1 parent fb28d76 commit 89687de

File tree

5 files changed

+186
-15
lines changed

5 files changed

+186
-15
lines changed

src/hooks/Cache.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,11 +181,10 @@ describe('Cache', () => {
181181
promise = result;
182182
}
183183

184-
await expect(promise).resolves.toBeUndefined();
184+
await expect(promise).resolves.toBe('fallback');
185185

186186
expect(cache.load(options)).toEqual('fallback');
187187

188-
// Should cache the result but not the fallback value
189188
expect(cache.load({...options, fallback: 'error'})).toEqual('error');
190189

191190
expect(loader).toHaveBeenCalledTimes(1);

src/hooks/Cache.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ export class Cache {
3838
return fallback;
3939
}
4040

41-
throw cachedEntry.error;
41+
if (cachedEntry.result === undefined) {
42+
throw cachedEntry.error;
43+
}
4244
}
4345

4446
if (cachedEntry.result !== undefined) {
@@ -68,7 +70,10 @@ export class Cache {
6870
return result;
6971
})
7072
.catch(error => {
73+
entry.result = fallback;
7174
entry.error = error;
75+
76+
return fallback;
7277
})
7378
.finally(() => {
7479
entry.dispose();

src/hooks/useContent.ssr.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {renderHook} from '@testing-library/react';
2+
import {getSlotContent} from '@croct/content';
23
import {useContent} from './useContent';
34

45
jest.mock(
@@ -9,7 +10,19 @@ jest.mock(
910
}),
1011
);
1112

13+
jest.mock(
14+
'@croct/content',
15+
() => ({
16+
__esModule: true,
17+
getSlotContent: jest.fn().mockReturnValue(null),
18+
}),
19+
);
20+
1221
describe('useContent (SSR)', () => {
22+
beforeEach(() => {
23+
jest.clearAllMocks();
24+
});
25+
1326
it('should render the initial value on the server-side', () => {
1427
const {result} = renderHook(() => useContent('slot-id', {initial: 'foo'}));
1528

@@ -20,4 +33,35 @@ describe('useContent (SSR)', () => {
2033
expect(() => useContent('slot-id'))
2134
.toThrow('The initial content is required for server-side rendering (SSR).');
2235
});
36+
37+
it('should use the default content as initial value on the server-side if not provided', () => {
38+
const content = {foo: 'bar'};
39+
const slotId = 'slot-id';
40+
const preferredLocale = 'en';
41+
42+
jest.mocked(getSlotContent).mockReturnValue(content);
43+
44+
const {result} = renderHook(() => useContent(slotId, {preferredLocale: preferredLocale}));
45+
46+
expect(getSlotContent).toHaveBeenCalledWith(slotId, preferredLocale);
47+
48+
expect(result.current).toBe(content);
49+
});
50+
51+
it('should use the provided initial value on the server-side', () => {
52+
const initial = null;
53+
const slotId = 'slot-id';
54+
const preferredLocale = 'en';
55+
56+
jest.mocked(getSlotContent).mockReturnValue(null);
57+
58+
const {result} = renderHook(
59+
() => useContent(slotId, {
60+
preferredLocale: preferredLocale,
61+
initial: initial,
62+
}),
63+
);
64+
65+
expect(result.current).toBe(initial);
66+
});
2367
});

src/hooks/useContent.test.ts

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {renderHook, waitFor} from '@testing-library/react';
2+
import {getSlotContent} from '@croct/content';
23
import {Plug} from '@croct/plug';
34
import {useCroct} from './useCroct';
45
import {useLoader} from './useLoader';
@@ -19,6 +20,14 @@ jest.mock(
1920
}),
2021
);
2122

23+
jest.mock(
24+
'@croct/content',
25+
() => ({
26+
__esModule: true,
27+
getSlotContent: jest.fn().mockReturnValue(null),
28+
}),
29+
);
30+
2231
describe('useContent (CSR)', () => {
2332
beforeEach(() => {
2433
jest.resetAllMocks();
@@ -54,9 +63,6 @@ describe('useContent (CSR)', () => {
5463
expect(useCroct).toHaveBeenCalled();
5564
expect(useLoader).toHaveBeenCalledWith({
5665
cacheKey: hash(`useContent:${cacheKey}:${slotId}:${preferredLocale}:${JSON.stringify(attributes)}`),
57-
fallback: {
58-
title: 'error',
59-
},
6066
expiration: 50,
6167
loader: expect.any(Function),
6268
});
@@ -67,6 +73,7 @@ describe('useContent (CSR)', () => {
6773
.loader();
6874

6975
expect(fetch).toHaveBeenCalledWith(slotId, {
76+
fallback: {title: 'error'},
7077
preferredLocale: 'en',
7178
attributes: attributes,
7279
});
@@ -180,4 +187,107 @@ describe('useContent (CSR)', () => {
180187

181188
await waitFor(() => expect(result.current).toEqual({title: 'second'}));
182189
});
190+
191+
it('should use the default content as initial value if not provided', () => {
192+
const content = {foo: 'bar'};
193+
const slotId = 'slot-id';
194+
const preferredLocale = 'en';
195+
196+
jest.mocked(getSlotContent).mockReturnValue(content);
197+
198+
renderHook(() => useContent(slotId, {preferredLocale: preferredLocale}));
199+
200+
expect(getSlotContent).toHaveBeenCalledWith(slotId, preferredLocale);
201+
202+
expect(useLoader).toHaveBeenCalledWith(
203+
expect.objectContaining({
204+
initial: content,
205+
}),
206+
);
207+
});
208+
209+
it('should use the provided initial value', () => {
210+
const initial = null;
211+
const slotId = 'slot-id';
212+
const preferredLocale = 'en';
213+
214+
jest.mocked(getSlotContent).mockReturnValue(null);
215+
216+
renderHook(
217+
() => useContent(slotId, {
218+
preferredLocale: preferredLocale,
219+
initial: initial,
220+
}),
221+
);
222+
223+
expect(useLoader).toHaveBeenCalledWith(
224+
expect.objectContaining({
225+
initial: initial,
226+
}),
227+
);
228+
});
229+
230+
it('should use the default content as fallback value if not provided', () => {
231+
const content = {foo: 'bar'};
232+
const slotId = 'slot-id';
233+
const preferredLocale = 'en';
234+
235+
const fetch: Plug['fetch'] = jest.fn().mockResolvedValue({
236+
content: {},
237+
});
238+
239+
jest.mocked(useCroct).mockReturnValue({fetch: fetch} as Plug);
240+
241+
jest.mocked(getSlotContent).mockReturnValue(content);
242+
243+
renderHook(
244+
() => useContent(slotId, {
245+
preferredLocale: preferredLocale,
246+
fallback: content,
247+
}),
248+
);
249+
250+
expect(getSlotContent).toHaveBeenCalledWith(slotId, preferredLocale);
251+
252+
jest.mocked(useLoader)
253+
.mock
254+
.calls[0][0]
255+
.loader();
256+
257+
expect(fetch).toHaveBeenCalledWith(slotId, {
258+
fallback: content,
259+
preferredLocale: preferredLocale,
260+
});
261+
});
262+
263+
it('should use the provided fallback value', () => {
264+
const fallback = null;
265+
const slotId = 'slot-id';
266+
const preferredLocale = 'en';
267+
268+
const fetch: Plug['fetch'] = jest.fn().mockResolvedValue({
269+
content: {},
270+
});
271+
272+
jest.mocked(useCroct).mockReturnValue({fetch: fetch} as Plug);
273+
274+
jest.mocked(getSlotContent).mockReturnValue(null);
275+
276+
renderHook(
277+
() => useContent(slotId, {
278+
preferredLocale: preferredLocale,
279+
fallback: fallback,
280+
}),
281+
);
282+
283+
jest.mocked(useLoader)
284+
.mock
285+
.calls[0][0]
286+
.loader();
287+
288+
expect(fetch).toHaveBeenCalledWith(slotId, {
289+
fallback: fallback,
290+
preferredLocale: preferredLocale,
291+
});
292+
});
183293
});

src/hooks/useContent.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import {SlotContent, VersionedSlotId, VersionedSlotMap} from '@croct/plug/slot';
22
import {JsonObject} from '@croct/plug/sdk/json';
33
import {FetchOptions} from '@croct/plug/plug';
4-
import {useEffect, useState} from 'react';
4+
import {useEffect, useMemo, useState} from 'react';
5+
import {getSlotContent} from '@croct/content';
56
import {useLoader} from './useLoader';
67
import {useCroct} from './useCroct';
78
import {isSsr} from '../ssr-polyfills';
89
import {hash} from '../hash';
910

1011
export type UseContentOptions<I, F> = FetchOptions<F> & {
12+
fallback?: F,
1113
initial?: I,
1214
cacheKey?: string,
1315
expiration?: number,
@@ -19,16 +21,24 @@ function useCsrContent<I, F>(
1921
options: UseContentOptions<I, F> = {},
2022
): SlotContent | I | F {
2123
const {
22-
fallback,
2324
cacheKey,
2425
expiration,
26+
fallback: fallbackContent,
2527
initial: initialContent,
2628
staleWhileLoading = false,
2729
...fetchOptions
2830
} = options;
2931

30-
const [initial, setInitial] = useState<SlotContent | I | F | undefined>(initialContent);
3132
const {preferredLocale} = fetchOptions;
33+
const defaultContent = useMemo(
34+
() => getSlotContent(id, preferredLocale) as SlotContent|null ?? undefined,
35+
[id, preferredLocale],
36+
);
37+
const fallback = fallbackContent === undefined ? defaultContent : fallbackContent;
38+
const [initial, setInitial] = useState<SlotContent | I | F | undefined>(
39+
() => (initialContent === undefined ? defaultContent : initialContent),
40+
);
41+
3242
const croct = useCroct();
3343

3444
const result: SlotContent | I | F = useLoader({
@@ -38,9 +48,8 @@ function useCsrContent<I, F>(
3848
+ `:${preferredLocale ?? ''}`
3949
+ `:${JSON.stringify(fetchOptions.attributes ?? {})}`,
4050
),
41-
loader: () => croct.fetch(id, fetchOptions).then(({content}) => content),
51+
loader: () => croct.fetch(id, {...fetchOptions, fallback: fallback}).then(({content}) => content),
4252
initial: initial,
43-
fallback: fallback,
4453
expiration: expiration,
4554
});
4655

@@ -63,17 +72,21 @@ function useCsrContent<I, F>(
6372
}
6473

6574
function useSsrContent<I, F>(
66-
_: VersionedSlotId,
67-
{initial}: UseContentOptions<I, F> = {},
75+
slotId: VersionedSlotId,
76+
{initial, preferredLocale}: UseContentOptions<I, F> = {},
6877
): SlotContent | I | F {
69-
if (initial === undefined) {
78+
const resolvedInitialContent = initial === undefined
79+
? getSlotContent(slotId, preferredLocale) as I|null ?? undefined
80+
: initial;
81+
82+
if (resolvedInitialContent === undefined) {
7083
throw new Error(
7184
'The initial content is required for server-side rendering (SSR). '
7285
+ 'For help, see https://croct.help/sdk/react/missing-slot-content',
7386
);
7487
}
7588

76-
return initial;
89+
return resolvedInitialContent;
7790
}
7891

7992
type UseContentHook = {

0 commit comments

Comments
 (0)