diff --git a/packages/docusaurus-theme-common/src/hooks/useSearchPage.ts b/packages/docusaurus-theme-common/src/hooks/useSearchPage.ts index 1b22512f58..d23a43fefe 100644 --- a/packages/docusaurus-theme-common/src/hooks/useSearchPage.ts +++ b/packages/docusaurus-theme-common/src/hooks/useSearchPage.ts @@ -5,32 +5,24 @@ * LICENSE file in the root directory of this source tree. */ -import {useCallback, useEffect, useState} from 'react'; -import {useHistory} from '@docusaurus/router'; +import {useCallback} from 'react'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import {useQueryString} from '../utils/historyUtils'; import type {ThemeConfig as AlgoliaThemeConfig} from '@docusaurus/theme-search-algolia'; const SEARCH_PARAM_QUERY = 'q'; -/** Some utility functions around search queries. */ -export function useSearchPage(): { - /** - * Works hand-in-hand with `setSearchQuery`; whatever the user has inputted - * into the search box. - */ - searchQuery: string; - /** - * Set a new search query. In addition to updating `searchQuery`, this handle - * also mutates the location and appends the query. - */ - setSearchQuery: (newSearchQuery: string) => void; - /** - * Given a query, this handle generates the corresponding search page link, - * with base URL prepended. - */ - generateSearchPageLink: (targetSearchQuery: string) => string; -} { - const history = useHistory(); +/** + * Permits to read/write the current search query string + */ +export function useSearchQueryString(): [string, (newValue: string) => void] { + return useQueryString(SEARCH_PARAM_QUERY); +} + +/** + * Permits to create links to the search page with the appropriate query string + */ +export function useSearchLinkCreator(): (searchValue: string) => string { const { siteConfig: {baseUrl, themeConfig}, } = useDocusaurusContext(); @@ -38,47 +30,13 @@ export function useSearchPage(): { algolia: {searchPagePath}, } = themeConfig as AlgoliaThemeConfig; - const [searchQuery, setSearchQueryState] = useState(''); - - // Init search query just after React hydration - useEffect(() => { - const searchQueryStringValue = - new URLSearchParams(window.location.search).get(SEARCH_PARAM_QUERY) ?? ''; - - setSearchQueryState(searchQueryStringValue); - }, []); - - const setSearchQuery = useCallback( - (newSearchQuery: string) => { - const searchParams = new URLSearchParams(window.location.search); - - if (newSearchQuery) { - searchParams.set(SEARCH_PARAM_QUERY, newSearchQuery); - } else { - searchParams.delete(SEARCH_PARAM_QUERY); - } - - history.replace({ - search: searchParams.toString(), - }); - setSearchQueryState(newSearchQuery); - }, - [history], - ); - - const generateSearchPageLink = useCallback( - (targetSearchQuery: string) => + return useCallback( + (searchValue: string) => // Refer to https://github.com/facebook/docusaurus/pull/2838 // Note: if searchPagePath is falsy, useSearchPage() will not be called `${baseUrl}${ searchPagePath as string - }?${SEARCH_PARAM_QUERY}=${encodeURIComponent(targetSearchQuery)}`, + }?${SEARCH_PARAM_QUERY}=${encodeURIComponent(searchValue)}`, [baseUrl, searchPagePath], ); - - return { - searchQuery, - setSearchQuery, - generateSearchPageLink, - }; } diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index 600c75778c..0fdda7f3aa 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -73,6 +73,11 @@ export { type TagLetterEntry, } from './utils/tagsUtils'; +export { + useSearchQueryString, + useSearchLinkCreator, +} from './hooks/useSearchPage'; + export {isMultiColumnFooterLinks} from './utils/footerUtils'; export {isRegexpStringMatch} from './utils/regexpUtils'; diff --git a/packages/docusaurus-theme-common/src/internal.ts b/packages/docusaurus-theme-common/src/internal.ts index 4298d3e985..1814712856 100644 --- a/packages/docusaurus-theme-common/src/internal.ts +++ b/packages/docusaurus-theme-common/src/internal.ts @@ -121,7 +121,6 @@ export { keyboardFocusedClassName, } from './hooks/useKeyboardNavigation'; export {useLockBodyScroll} from './hooks/useLockBodyScroll'; -export {useSearchPage} from './hooks/useSearchPage'; export {useCodeWordWrap} from './hooks/useCodeWordWrap'; export {getPrismCssVariables} from './utils/codeBlockUtils'; export {useBackToTopButton} from './hooks/useBackToTopButton'; diff --git a/packages/docusaurus-theme-common/src/utils/historyUtils.ts b/packages/docusaurus-theme-common/src/utils/historyUtils.ts index 06b124a229..686a876f84 100644 --- a/packages/docusaurus-theme-common/src/utils/historyUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/historyUtils.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {useEffect} from 'react'; +import {useCallback, useEffect} from 'react'; import {useHistory} from '@docusaurus/router'; // @ts-expect-error: TODO temporary until React 18 upgrade import {useSyncExternalStore} from 'use-sync-external-store/shim'; @@ -75,3 +75,42 @@ export function useQueryStringValue(key: string | null): string | null { return new URLSearchParams(history.location.search).get(key); }); } + +export function useQueryStringKeySetter(): ( + key: string, + newValue: string | null, + options?: {push: boolean}, +) => void { + const history = useHistory(); + return useCallback( + (key, newValue, options) => { + const searchParams = new URLSearchParams(history.location.search); + if (newValue) { + searchParams.set(key, newValue); + } else { + searchParams.delete(key); + } + const updaterFn = options?.push ? history.push : history.replace; + updaterFn({ + search: searchParams.toString(), + }); + }, + [history], + ); +} + +export function useQueryString( + key: string, +): [string, (newValue: string, options?: {push: boolean}) => void] { + const value = useQueryStringValue(key) ?? ''; + const setQueryString = useQueryStringKeySetter(); + return [ + value, + useCallback( + (newValue: string, options) => { + setQueryString(key, newValue, options); + }, + [setQueryString, key], + ), + ]; +} diff --git a/packages/docusaurus-theme-search-algolia/src/theme/SearchBar/index.tsx b/packages/docusaurus-theme-search-algolia/src/theme/SearchBar/index.tsx index b0456492d4..0f4d7ee000 100644 --- a/packages/docusaurus-theme-search-algolia/src/theme/SearchBar/index.tsx +++ b/packages/docusaurus-theme-search-algolia/src/theme/SearchBar/index.tsx @@ -10,8 +10,10 @@ import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react'; import Head from '@docusaurus/Head'; import Link from '@docusaurus/Link'; import {useHistory} from '@docusaurus/router'; -import {isRegexpStringMatch} from '@docusaurus/theme-common'; -import {useSearchPage} from '@docusaurus/theme-common/internal'; +import { + isRegexpStringMatch, + useSearchLinkCreator, +} from '@docusaurus/theme-common'; import { useAlgoliaContextualFacetFilters, useSearchResultUrlProcessor, @@ -59,10 +61,10 @@ type ResultsFooterProps = { }; function ResultsFooter({state, onClose}: ResultsFooterProps) { - const {generateSearchPageLink} = useSearchPage(); + const createSearchLink = useSearchLinkCreator(); return ( - + diff --git a/packages/docusaurus-theme-search-algolia/src/theme/SearchPage/index.tsx b/packages/docusaurus-theme-search-algolia/src/theme/SearchPage/index.tsx index 1c3db1eb0d..323fb4f458 100644 --- a/packages/docusaurus-theme-search-algolia/src/theme/SearchPage/index.tsx +++ b/packages/docusaurus-theme-search-algolia/src/theme/SearchPage/index.tsx @@ -21,11 +21,9 @@ import { HtmlClassNameProvider, useEvent, usePluralForm, + useSearchQueryString, } from '@docusaurus/theme-common'; -import { - useSearchPage, - useTitleFormatter, -} from '@docusaurus/theme-common/internal'; +import {useTitleFormatter} from '@docusaurus/theme-common/internal'; import Translate, {translate} from '@docusaurus/Translate'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import { @@ -167,7 +165,7 @@ function SearchPageContent(): JSX.Element { const documentsFoundPlural = useDocumentsFoundPlural(); const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers(); - const {searchQuery, setSearchQuery} = useSearchPage(); + const [searchQuery, setSearchQuery] = useSearchQueryString(); const initialSearchResultState: ResultDispatcherState = { items: [], query: null,