From 8b4886d257d2ba9d965eb11bee06b9116652ef84 Mon Sep 17 00:00:00 2001 From: LooseLi <1329307562@qq.com> Date: Wed, 7 May 2025 17:04:45 +0800 Subject: [PATCH] fix: replace throttle with debounce to fix infinite loading issue --- packages/react-notion-x/package.json | 2 + .../src/components/search-dialog.tsx | 147 +++++++++++------- 2 files changed, 90 insertions(+), 59 deletions(-) diff --git a/packages/react-notion-x/package.json b/packages/react-notion-x/package.json index 5f62c66e2..3586a95c8 100644 --- a/packages/react-notion-x/package.json +++ b/packages/react-notion-x/package.json @@ -68,6 +68,7 @@ "react-modal": "^3.16.3" }, "devDependencies": { + "@types/lodash.debounce": "^4.0.9", "@types/lodash.throttle": "^4.1.6", "@types/prismjs": "^1.26.5", "@types/react": "^19.0.11", @@ -75,6 +76,7 @@ "clipboard-copy": "^4.0.1", "date-fns": "^4.1.0", "format-number": "^3.0.0", + "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1", "react-pdf": "^9.1.1" }, diff --git a/packages/react-notion-x/src/components/search-dialog.tsx b/packages/react-notion-x/src/components/search-dialog.tsx index d930416c3..b44a50f26 100644 --- a/packages/react-notion-x/src/components/search-dialog.tsx +++ b/packages/react-notion-x/src/components/search-dialog.tsx @@ -1,5 +1,5 @@ import type * as types from 'notion-types' -import throttle from 'lodash.throttle' +import debounce from 'lodash.debounce' import { getBlockParentPage, getBlockTitle } from 'notion-utils' import * as React from 'react' @@ -42,7 +42,9 @@ export class SearchDialog extends React.Component<{ _search: any componentDidMount() { - this._search = throttle(this._searchImpl.bind(this), 1000) + this._search = debounce(this._searchImpl.bind(this), 1000, { + trailing: true + }) this._warmupSearch() } @@ -173,14 +175,22 @@ export class SearchDialog extends React.Component<{ _onChangeQuery = (e: any) => { const query = e.target.value - this.setState({ query }) if (!query.trim()) { - this.setState({ isLoading: false, searchResult: null, searchError: null }) + this.setState({ + query, + isLoading: false, + searchResult: null, + searchError: null + }) return - } else { - this._search() } + + // set query and trigger search, but don't immediately change loading state + this.setState({ query }, () => { + // trigger search after state update + this._search() + }) } _onClearQuery = () => { @@ -208,63 +218,82 @@ export class SearchDialog extends React.Component<{ return } - this.setState({ isLoading: true }) - const result: any = await searchNotion({ - query, - ancestorId: rootBlockId - }) - - console.log('search', query, result) - - let searchResult: any = null // TODO - let searchError: types.APIError | null = null + // store current query for later comparison + const currentQuery = query - if (result.error || result.errorId) { - searchError = result - } else { - searchResult = { ...result } - - const results = searchResult.results - .map((result: any) => { - const block = searchResult.recordMap.block[result.id]?.value - if (!block) return - - const title = getBlockTitle(block, searchResult.recordMap) - if (!title) { - return - } - - result.title = title - result.block = block - result.recordMap = searchResult.recordMap - result.page = - getBlockParentPage(block, searchResult.recordMap, { - inclusive: true - }) || block - - if (!result.page.id) { - return - } + this.setState({ isLoading: true }) - if (result.highlight?.text) { - result.highlight.html = result.highlight.text - .replaceAll(//gi, '') - .replaceAll(/<\/gzknfouu>/gi, '') + try { + const result: any = await searchNotion({ + query: currentQuery, + ancestorId: rootBlockId + }) + + console.log('search', currentQuery, result) + + let searchResult: any = null // TODO + let searchError: types.APIError | null = null + + if (result.error || result.errorId) { + searchError = result + } else { + searchResult = { ...result } + + const results = searchResult.results + .map((result: any) => { + const block = searchResult.recordMap.block[result.id]?.value + if (!block) return + + const title = getBlockTitle(block, searchResult.recordMap) + if (!title) { + return + } + + result.title = title + result.block = block + result.recordMap = searchResult.recordMap + result.page = + getBlockParentPage(block, searchResult.recordMap, { + inclusive: true + }) || block + + if (!result.page.id) { + return + } + + if (result.highlight?.text) { + result.highlight.html = result.highlight.text + .replaceAll(//gi, '') + .replaceAll(/<\/gzknfouu>/gi, '') + } + + return result + }) + .filter(Boolean) + + // dedupe results by page id + const searchResultsMap = Object.fromEntries( + results.map((result: any) => [result.page.id, result]) + ) + searchResult.results = Object.values(searchResultsMap) + } + + // ensure state is only updated when current query matches the state query + if (this.state.query === currentQuery) { + this.setState({ isLoading: false, searchResult, searchError }) + } + } catch (err) { + console.error('err:', err) + if (this.state.query === currentQuery) { + this.setState({ + isLoading: false, + searchResult: null, + searchError: { + message: 'search_error', + errorId: 'search_error' } - - return result }) - .filter(Boolean) - - // dedupe results by page id - const searchResultsMap = Object.fromEntries( - results.map((result: any) => [result.page.id, result]) - ) - searchResult.results = Object.values(searchResultsMap) - } - - if (this.state.query === query) { - this.setState({ isLoading: false, searchResult, searchError }) + } } } }