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/ype-5528-hooks-split.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@youversion/platform-react-hooks': patch
---

Hooks import `YouVersionContext` from its module, not the context barrel, so a `useChapter` import does not evaluate Auth. Hooks run tsup's Rollup tree-shake pass so unused exports drop from narrow graphs. Hooks wipe `dist` on build so stale tsup `.d.cts` files cannot publish.
2 changes: 1 addition & 1 deletion .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"name": "hooks / useChapter only",
"path": "packages/hooks/dist/index.js",
"import": "{ useChapter }",
"limit": "64 KB",
"limit": "12.2 KB",
"ignore": ["react", "react-dom", "@tanstack/react-query"]
},
{
Expand Down
10 changes: 6 additions & 4 deletions packages/hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"CHANGELOG.md",
"!dist/**/*.test.*",
"!dist/__tests__",
"!dist/test"
"!dist/test",
"!dist/**/*.d.ts.map",
"!dist/**/*.d.cts"
],
"publishConfig": {
"access": "public",
Expand All @@ -38,10 +40,10 @@
}
},
"scripts": {
"dev": "tsup src/index.ts src/test-utils.tsx --format cjs,esm --external jsdom --watch",
"build:js": "tsup src/index.ts src/test-utils.tsx --format cjs,esm --external jsdom",
"dev": "tsup --watch",
"build:js": "tsup",
"build:types": "tsc -p tsconfig.build.json",
"build": "pnpm build:js && pnpm build:types",
"build": "rm -rf dist && pnpm build:js && pnpm build:types",
Comment thread
cameronapak marked this conversation as resolved.
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
Expand Down
11 changes: 9 additions & 2 deletions packages/hooks/src/context/YouVersionProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
'use client';

import type { PropsWithChildren, ReactNode } from 'react';
import type { LazyExoticComponent, PropsWithChildren, ReactNode } from 'react';
import { lazy, Suspense, useEffect, useMemo, useState } from 'react';
import { QueryClient } from '@tanstack/react-query';
import { YouVersionContext } from './YouVersionContext';
import type YouVersionAuthProvider from './YouVersionAuthProvider';
import { serializeAdditionalHeaders } from '../internal/additionalHeadersKey';
import { InternalQueryClientProvider } from '../internal/QueryClientContext';
import { queryClientDefaultOptions } from '../internal/queryClientDefaults';
Expand Down Expand Up @@ -72,7 +73,12 @@ interface YouVersionProviderPropsWithoutAuth extends YouVersionProviderPropsBase
authRedirectUrl?: never;
}

const AuthProvider = lazy(() => import('./YouVersionAuthProvider'));
let authProvider: LazyExoticComponent<typeof YouVersionAuthProvider> | undefined;

function getAuthProvider(): LazyExoticComponent<typeof YouVersionAuthProvider> {
authProvider ??= lazy(() => import('./YouVersionAuthProvider'));
return authProvider;
}

function useResolvedTheme(theme: 'light' | 'dark' | 'system'): 'light' | 'dark' {
const [resolved, setResolved] = useState<'light' | 'dark'>(() => {
Expand Down Expand Up @@ -196,6 +202,7 @@ function YouVersionProviderInner(
};

if (includeAuth) {
const AuthProvider = getAuthProvider();
return (
<YouVersionContext.Provider value={contextValue}>
<InternalQueryClientProvider client={queryClient}>
Expand Down
2 changes: 1 addition & 1 deletion packages/hooks/src/internal/useApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useContext, useMemo } from 'react';
import { ApiClient } from '@youversion/platform-core';
import { YouVersionContext } from '../context';
import { YouVersionContext } from '../context/YouVersionContext';

/**
* @internal
Expand Down
2 changes: 1 addition & 1 deletion packages/hooks/src/internal/useQueryKeyBase.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { useContext, useMemo } from 'react';
import { YouVersionContext } from '../context';
import { YouVersionContext } from '../context/YouVersionContext';
import { serializeAdditionalHeaders } from './additionalHeadersKey';
import { serializeVersionFilters } from './versionFilterKey';

Expand Down
12 changes: 7 additions & 5 deletions packages/hooks/src/internal/versionFilterKey.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { YouVersionPlatformConfiguration } from '@youversion/platform-core';
import { getVersionFilterSnapshot } from '@youversion/platform-core';

/**
* @internal
* Converts the active Bible version filter to stable text.
*
* The filter lives on `YouVersionPlatformConfiguration`, which `YouVersionProvider`
* writes from its `permittedVersionIds`, `excludedVersionIds`, and
* `permittedLanguageTags` props during render, before children read this value.
* `YouVersionProvider` writes its `permittedVersionIds`, `excludedVersionIds`,
* and `permittedLanguageTags` props onto `YouVersionPlatformConfiguration`
* during render, before children read this value. Readers use
* `getVersionFilterSnapshot` so unused auth storage stays off the `useChapter`
* graph.
*
* The ids and tags are sorted before serialization, so two lists with the same
* entries produce the same text. An unset list serializes as `null`, which keeps
Expand All @@ -18,7 +20,7 @@ import { YouVersionPlatformConfiguration } from '@youversion/platform-core';
*/
export function serializeVersionFilters(): string | null {
const { permittedVersionIds, excludedVersionIds, permittedLanguageTags } =
YouVersionPlatformConfiguration;
getVersionFilterSnapshot();

if (
permittedVersionIds === undefined &&
Expand Down
4 changes: 2 additions & 2 deletions packages/hooks/src/useApiData.ssr.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import { renderToString } from 'react-dom/server';
import { describe, expect, it, vi } from 'vitest';
import { BibleClient } from '@youversion/platform-core';
import * as core from '@youversion/platform-core';
import { YouVersionProvider } from './context/YouVersionProvider';
import { useChapter } from './useChapter';

Expand All @@ -20,7 +20,7 @@ function Chapter() {
describe('useApiData — server-side rendering', () => {
it('renders the loading state without crashing or fetching', () => {
const getChapter = vi
.spyOn(BibleClient.prototype, 'getChapter')
.spyOn(core, 'getChapter')
.mockRejectedValue(new Error('must not be called during SSR'));

const html = renderToString(
Expand Down
2 changes: 1 addition & 1 deletion packages/hooks/src/useBibleClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useContext, useMemo } from 'react';
import { BibleClient } from '@youversion/platform-core';
import { YouVersionContext } from './context';
import { YouVersionContext } from './context/YouVersionContext';
import { useApiClient } from './internal/useApiClient';

export function useBibleClient(): BibleClient {
Expand Down
12 changes: 8 additions & 4 deletions packages/hooks/src/useBook.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
'use client';

import { useBibleClient } from './useBibleClient';
import { useContext } from 'react';
import { getBook, type BibleBook } from '@youversion/platform-core';
import { YouVersionContext } from './context/YouVersionContext';
import { useApiClient } from './internal/useApiClient';
import { useApiData, type UseApiDataOptions } from './useApiData';
import { useQueryKeyBase } from './internal/useQueryKeyBase';
import type { UseNamedQueryResult } from './useQueryResult';
import type { BibleBook } from '@youversion/platform-core';

export type UseBookResult = UseNamedQueryResult<'book', BibleBook>;

Expand All @@ -13,12 +15,14 @@ export function useBook(
book: string,
options?: UseApiDataOptions,
): UseBookResult {
const bibleClient = useBibleClient();
const bibleClient = useContext(YouVersionContext)?.bibleClient;
const apiClient = useApiClient();
const keyBase = useQueryKeyBase();

const { data, loading, error, refetch } = useApiData<BibleBook>(
[...keyBase, 'book', versionId, book],
() => bibleClient.getBook(versionId, book),
() =>
bibleClient ? bibleClient.getBook(versionId, book) : getBook(apiClient, versionId, book),
{
enabled: options?.enabled !== false,
keepPreviousData: options?.keepPreviousData,
Expand Down
11 changes: 7 additions & 4 deletions packages/hooks/src/useBooks.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
'use client';
import { useBibleClient } from './useBibleClient';
import { useContext } from 'react';
import { getBooks, type BibleBook, type Collection } from '@youversion/platform-core';
import { YouVersionContext } from './context/YouVersionContext';
import { useApiClient } from './internal/useApiClient';
import { useApiData, type UseApiDataOptions } from './useApiData';
import { useQueryKeyBase } from './internal/useQueryKeyBase';
import type { UseNamedQueryResult } from './useQueryResult';
import type { BibleBook, Collection } from '@youversion/platform-core';
import { useHookOverride } from './useHookOverride';

export type UseBooksResult = UseNamedQueryResult<'books', Collection<BibleBook>>;

export function useBooks(versionId: number, options?: UseApiDataOptions): UseBooksResult {
const override = useHookOverride('useBooks');
const bibleClient = useBibleClient();
const bibleClient = useContext(YouVersionContext)?.bibleClient;
const apiClient = useApiClient();
const keyBase = useQueryKeyBase();

const {
Expand All @@ -20,7 +23,7 @@ export function useBooks(versionId: number, options?: UseApiDataOptions): UseBoo
refetch,
} = useApiData<Collection<BibleBook>>(
[...keyBase, 'books', versionId],
() => bibleClient.getBooks(versionId),
() => (bibleClient ? bibleClient.getBooks(versionId) : getBooks(apiClient, versionId)),
{
enabled: !override && options?.enabled !== false,
keepPreviousData: options?.keepPreviousData,
Expand Down
17 changes: 16 additions & 1 deletion packages/hooks/src/useChapter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { render, renderHook, waitFor, act } from '@testing-library/react';
import { describe, expect, vi, beforeEach, it } from 'vitest';
import { useChapter, type UseChapterResult } from './useChapter';
import { YouVersionContext } from './context';
import { type BibleChapter } from '@youversion/platform-core';
import * as core from '@youversion/platform-core';
import type { BibleChapter } from '@youversion/platform-core';
import { createBibleClientStub, createYVWrapper, TestQueryClientProvider } from './test/utils';

describe('useChapter', () => {
Expand Down Expand Up @@ -160,6 +161,20 @@ describe('useChapter', () => {
});
});

it('fetches via getChapter when the provider has no bibleClient override', async () => {
const spy = vi.spyOn(core, 'getChapter').mockResolvedValue(mockChapter);
const bare = createYVWrapper('test-app-key');
const { result } = renderHook(() => useChapter(111, 'MAT', 1), { wrapper: bare });

await waitFor(() => {
expect(result.current.loading).toBe(false);
});

expect(spy).toHaveBeenCalledWith(expect.any(core.ApiClient), 111, 'MAT', 1);
expect(result.current.chapter).toEqual(mockChapter);
spy.mockRestore();
});

it('should serve the cached chapter instantly on revisit and revalidate in background', async () => {
// The acceptance criterion behind the TanStack Query migration: leaving
// a chapter and coming back renders it from cache with no loading
Expand Down
14 changes: 10 additions & 4 deletions packages/hooks/src/useChapter.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'use client';
import { useBibleClient } from './useBibleClient';
import { useContext } from 'react';
import { getChapter, type BibleChapter } from '@youversion/platform-core';
import { YouVersionContext } from './context/YouVersionContext';
import { useApiClient } from './internal/useApiClient';
import { useApiData, type UseApiDataOptions } from './useApiData';
import { useQueryKeyBase } from './internal/useQueryKeyBase';
import type { UseNamedQueryResult } from './useQueryResult';
import type { BibleChapter } from '@youversion/platform-core';

export type UseChapterResult = UseNamedQueryResult<'chapter', BibleChapter>;

Expand All @@ -13,7 +15,8 @@ export function useChapter(
chapter: number,
options?: UseApiDataOptions,
): UseChapterResult {
const bibleClient = useBibleClient();
const bibleClient = useContext(YouVersionContext)?.bibleClient;
const apiClient = useApiClient();
const keyBase = useQueryKeyBase();

const {
Expand All @@ -23,7 +26,10 @@ export function useChapter(
refetch,
} = useApiData<BibleChapter>(
[...keyBase, 'chapter', versionId, book, chapter],
() => bibleClient.getChapter(versionId, book, chapter),
() =>
bibleClient
? bibleClient.getChapter(versionId, book, chapter)
: getChapter(apiClient, versionId, book, chapter),
{
enabled: options?.enabled !== false,
keepPreviousData: options?.keepPreviousData,
Expand Down
14 changes: 10 additions & 4 deletions packages/hooks/src/useChapters.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
'use client';

import { useBibleClient } from './useBibleClient';
import { useContext } from 'react';
import { getChapters, type BibleChapter, type Collection } from '@youversion/platform-core';
import { YouVersionContext } from './context/YouVersionContext';
import { useApiClient } from './internal/useApiClient';
import { useApiData, type UseApiDataOptions } from './useApiData';
import { useQueryKeyBase } from './internal/useQueryKeyBase';
import type { UseNamedQueryResult } from './useQueryResult';
import type { BibleChapter, Collection } from '@youversion/platform-core';

export type UseChaptersResult = UseNamedQueryResult<'chapters', Collection<BibleChapter>>;

Expand All @@ -13,7 +15,8 @@ export function useChapters(
book: string,
options?: UseApiDataOptions,
): UseChaptersResult {
const bibleClient = useBibleClient();
const bibleClient = useContext(YouVersionContext)?.bibleClient;
const apiClient = useApiClient();
const keyBase = useQueryKeyBase();

// Don't attempt to fetch if book is invalid
Expand All @@ -26,7 +29,10 @@ export function useChapters(
refetch,
} = useApiData<Collection<BibleChapter>>(
[...keyBase, 'chapters', versionId, book],
() => bibleClient.getChapters(versionId, book),
() =>
bibleClient
? bibleClient.getChapters(versionId, book)
: getChapters(apiClient, versionId, book),
{
enabled: options?.enabled !== false && isValidBook,
keepPreviousData: options?.keepPreviousData,
Expand Down
2 changes: 1 addition & 1 deletion packages/hooks/src/useHighlightAuthActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
YouVersionPlatformConfiguration,
type DataExchangeCallbackResult,
} from '@youversion/platform-core';
import { YouVersionContext } from './context';
import { YouVersionContext } from './context/YouVersionContext';
import { YouVersionAuthContext } from './context/YouVersionAuthContext';
import { useApiClient } from './internal/useApiClient';

Expand Down
18 changes: 17 additions & 1 deletion packages/hooks/src/useLanguage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { renderHook, waitFor, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useLanguage } from './useLanguage';
import * as core from '@youversion/platform-core';
import type { Language } from '@youversion/platform-core';
import { createLanguagesClientStub, createYVWrapper } from './test/utils';

describe('useLanguage', () => {
const mockGetLanguage = vi.fn();
const languagesClient = createLanguagesClientStub({ getLanguage: mockGetLanguage });
const wrapper = createYVWrapper('test-app-key', { languagesClient });

const mockLanguage = {
const mockLanguage: Language = {
id: 'en',
language: 'en',
script: 'Latn',
Expand Down Expand Up @@ -108,4 +110,18 @@ describe('useLanguage', () => {
});
});
});

it('fetches via getLanguage when the provider has no languagesClient override', async () => {
const spy = vi.spyOn(core, 'getLanguage').mockResolvedValue(mockLanguage);
const bare = createYVWrapper('test-app-key');
const { result } = renderHook(() => useLanguage('en'), { wrapper: bare });

await waitFor(() => {
expect(result.current.loading).toBe(false);
});

expect(spy).toHaveBeenCalledWith(expect.any(core.ApiClient), 'en');
expect(result.current.language).toEqual(mockLanguage);
spy.mockRestore();
});
});
14 changes: 10 additions & 4 deletions packages/hooks/src/useLanguage.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
'use client';

import { useContext } from 'react';
import { getLanguage, type Language } from '@youversion/platform-core';
import { YouVersionContext } from './context/YouVersionContext';
import { useApiClient } from './internal/useApiClient';
import { useApiData, type UseApiDataOptions } from './useApiData';
import { useQueryKeyBase } from './internal/useQueryKeyBase';
import { type Language } from '@youversion/platform-core';
import { useLanguagesClient } from './useLanguageClient';
import type { UseNamedQueryResult } from './useQueryResult';
import { useHookOverride } from './useHookOverride';

export type UseLanguageResult = UseNamedQueryResult<'language', Language>;

export function useLanguage(languageId: string, apiOptions?: UseApiDataOptions): UseLanguageResult {
const override = useHookOverride('useLanguage');
const languagesClient = useLanguagesClient();
const languagesClient = useContext(YouVersionContext)?.languagesClient;
const apiClient = useApiClient();
const keyBase = useQueryKeyBase();

const { data, loading, error, refetch } = useApiData<Language>(
[...keyBase, 'language', languageId],
() => languagesClient.getLanguage(languageId),
() =>
languagesClient
? languagesClient.getLanguage(languageId)
: getLanguage(apiClient, languageId),
{
enabled: !override && apiOptions?.enabled !== false,
keepPreviousData: apiOptions?.keepPreviousData,
Expand Down
Loading
Loading