Skip to content
Merged
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
3 changes: 1 addition & 2 deletions src/hooks/Cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,10 @@ describe('Cache', () => {
promise = result;
}

await expect(promise).resolves.toBeUndefined();
await expect(promise).resolves.toBe('fallback');

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

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

expect(loader).toHaveBeenCalledTimes(1);
Expand Down
7 changes: 6 additions & 1 deletion src/hooks/Cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export class Cache {
return fallback;
}

throw cachedEntry.error;
if (cachedEntry.result === undefined) {
throw cachedEntry.error;
}
}

if (cachedEntry.result !== undefined) {
Expand Down Expand Up @@ -68,7 +70,10 @@ export class Cache {
return result;
})
.catch(error => {
entry.result = fallback;
entry.error = error;

return fallback;
})
.finally(() => {
entry.dispose();
Expand Down
44 changes: 44 additions & 0 deletions src/hooks/useContent.ssr.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {renderHook} from '@testing-library/react';
import {getSlotContent} from '@croct/content';
import {useContent} from './useContent';

jest.mock(
Expand All @@ -9,7 +10,19 @@ jest.mock(
}),
);

jest.mock(
'@croct/content',
() => ({
__esModule: true,
getSlotContent: jest.fn().mockReturnValue(null),
}),
);

describe('useContent (SSR)', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should render the initial value on the server-side', () => {
const {result} = renderHook(() => useContent('slot-id', {initial: 'foo'}));

Expand All @@ -20,4 +33,35 @@ describe('useContent (SSR)', () => {
expect(() => useContent('slot-id'))
.toThrow('The initial content is required for server-side rendering (SSR).');
});

it('should use the default content as initial value on the server-side if not provided', () => {
const content = {foo: 'bar'};
const slotId = 'slot-id';
const preferredLocale = 'en';

jest.mocked(getSlotContent).mockReturnValue(content);

const {result} = renderHook(() => useContent(slotId, {preferredLocale: preferredLocale}));

expect(getSlotContent).toHaveBeenCalledWith(slotId, preferredLocale);

expect(result.current).toBe(content);
});

it('should use the provided initial value on the server-side', () => {
const initial = null;
const slotId = 'slot-id';
const preferredLocale = 'en';

jest.mocked(getSlotContent).mockReturnValue(null);

const {result} = renderHook(
() => useContent(slotId, {
preferredLocale: preferredLocale,
initial: initial,
}),
);

expect(result.current).toBe(initial);
});
});
116 changes: 113 additions & 3 deletions src/hooks/useContent.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {renderHook, waitFor} from '@testing-library/react';
import {getSlotContent} from '@croct/content';
import {Plug} from '@croct/plug';
import {useCroct} from './useCroct';
import {useLoader} from './useLoader';
Expand All @@ -19,6 +20,14 @@ jest.mock(
}),
);

jest.mock(
'@croct/content',
() => ({
__esModule: true,
getSlotContent: jest.fn().mockReturnValue(null),
}),
);

describe('useContent (CSR)', () => {
beforeEach(() => {
jest.resetAllMocks();
Expand Down Expand Up @@ -54,9 +63,6 @@ describe('useContent (CSR)', () => {
expect(useCroct).toHaveBeenCalled();
expect(useLoader).toHaveBeenCalledWith({
cacheKey: hash(`useContent:${cacheKey}:${slotId}:${preferredLocale}:${JSON.stringify(attributes)}`),
fallback: {
title: 'error',
},
expiration: 50,
loader: expect.any(Function),
});
Expand All @@ -67,6 +73,7 @@ describe('useContent (CSR)', () => {
.loader();

expect(fetch).toHaveBeenCalledWith(slotId, {
fallback: {title: 'error'},
preferredLocale: 'en',
attributes: attributes,
});
Expand Down Expand Up @@ -180,4 +187,107 @@ describe('useContent (CSR)', () => {

await waitFor(() => expect(result.current).toEqual({title: 'second'}));
});

it('should use the default content as initial value if not provided', () => {
const content = {foo: 'bar'};
const slotId = 'slot-id';
const preferredLocale = 'en';

jest.mocked(getSlotContent).mockReturnValue(content);

renderHook(() => useContent(slotId, {preferredLocale: preferredLocale}));

expect(getSlotContent).toHaveBeenCalledWith(slotId, preferredLocale);

expect(useLoader).toHaveBeenCalledWith(
expect.objectContaining({
initial: content,
}),
);
});

it('should use the provided initial value', () => {
const initial = null;
const slotId = 'slot-id';
const preferredLocale = 'en';

jest.mocked(getSlotContent).mockReturnValue(null);

renderHook(
() => useContent(slotId, {
preferredLocale: preferredLocale,
initial: initial,
}),
);

expect(useLoader).toHaveBeenCalledWith(
expect.objectContaining({
initial: initial,
}),
);
});

it('should use the default content as fallback value if not provided', () => {
const content = {foo: 'bar'};
const slotId = 'slot-id';
const preferredLocale = 'en';

const fetch: Plug['fetch'] = jest.fn().mockResolvedValue({
content: {},
});

jest.mocked(useCroct).mockReturnValue({fetch: fetch} as Plug);

jest.mocked(getSlotContent).mockReturnValue(content);

renderHook(
() => useContent(slotId, {
preferredLocale: preferredLocale,
fallback: content,
}),
);

expect(getSlotContent).toHaveBeenCalledWith(slotId, preferredLocale);

jest.mocked(useLoader)
.mock
.calls[0][0]
.loader();

expect(fetch).toHaveBeenCalledWith(slotId, {
fallback: content,
preferredLocale: preferredLocale,
});
});

it('should use the provided fallback value', () => {
const fallback = null;
const slotId = 'slot-id';
const preferredLocale = 'en';

const fetch: Plug['fetch'] = jest.fn().mockResolvedValue({
content: {},
});

jest.mocked(useCroct).mockReturnValue({fetch: fetch} as Plug);

jest.mocked(getSlotContent).mockReturnValue(null);

renderHook(
() => useContent(slotId, {
preferredLocale: preferredLocale,
fallback: fallback,
}),
);

jest.mocked(useLoader)
.mock
.calls[0][0]
.loader();

expect(fetch).toHaveBeenCalledWith(slotId, {
fallback: fallback,
preferredLocale: preferredLocale,
});
});
});
31 changes: 22 additions & 9 deletions src/hooks/useContent.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import {SlotContent, VersionedSlotId, VersionedSlotMap} from '@croct/plug/slot';
import {JsonObject} from '@croct/plug/sdk/json';
import {FetchOptions} from '@croct/plug/plug';
import {useEffect, useState} from 'react';
import {useEffect, useMemo, useState} from 'react';
import {getSlotContent} from '@croct/content';
import {useLoader} from './useLoader';
import {useCroct} from './useCroct';
import {isSsr} from '../ssr-polyfills';
import {hash} from '../hash';

export type UseContentOptions<I, F> = FetchOptions<F> & {
fallback?: F,
initial?: I,
cacheKey?: string,
expiration?: number,
Expand All @@ -19,16 +21,24 @@ function useCsrContent<I, F>(
options: UseContentOptions<I, F> = {},
): SlotContent | I | F {
const {
fallback,
cacheKey,
expiration,
fallback: fallbackContent,
initial: initialContent,
staleWhileLoading = false,
...fetchOptions
} = options;

const [initial, setInitial] = useState<SlotContent | I | F | undefined>(initialContent);
const {preferredLocale} = fetchOptions;
const defaultContent = useMemo(
() => getSlotContent(id, preferredLocale) as SlotContent|null ?? undefined,
[id, preferredLocale],
);
const fallback = fallbackContent === undefined ? defaultContent : fallbackContent;
const [initial, setInitial] = useState<SlotContent | I | F | undefined>(
() => (initialContent === undefined ? defaultContent : initialContent),
);

const croct = useCroct();

const result: SlotContent | I | F = useLoader({
Expand All @@ -38,9 +48,8 @@ function useCsrContent<I, F>(
+ `:${preferredLocale ?? ''}`
+ `:${JSON.stringify(fetchOptions.attributes ?? {})}`,
),
loader: () => croct.fetch(id, fetchOptions).then(({content}) => content),
loader: () => croct.fetch(id, {...fetchOptions, fallback: fallback}).then(({content}) => content),
initial: initial,
fallback: fallback,
expiration: expiration,
});

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

function useSsrContent<I, F>(
_: VersionedSlotId,
{initial}: UseContentOptions<I, F> = {},
slotId: VersionedSlotId,
{initial, preferredLocale}: UseContentOptions<I, F> = {},
): SlotContent | I | F {
if (initial === undefined) {
const resolvedInitialContent = initial === undefined
? getSlotContent(slotId, preferredLocale) as I|null ?? undefined
: initial;

if (resolvedInitialContent === undefined) {
throw new Error(
'The initial content is required for server-side rendering (SSR). '
+ 'For help, see https://croct.help/sdk/react/missing-slot-content',
);
}

return initial;
return resolvedInitialContent;
}

type UseContentHook = {
Expand Down