diff --git a/onboarding/src/Components/Steps/SiteList.js b/onboarding/src/Components/Steps/SiteList.js index fb818039..563b4827 100644 --- a/onboarding/src/Components/Steps/SiteList.js +++ b/onboarding/src/Components/Steps/SiteList.js @@ -17,6 +17,11 @@ import { get, track } from '../../utils/rest'; const { onboarding } = tiobDash; +// Debounce the search so it stays off the network while typing: only fire after the +// field is idle for SEARCH_DEBOUNCE_MS and the query is at least SEARCH_MIN_CHARS. +const SEARCH_DEBOUNCE_MS = 700; +const SEARCH_MIN_CHARS = 3; + // Guards tracking-session init against firing twice while the first request is in flight. let trackingInitStarted = false; @@ -89,7 +94,6 @@ const SiteList = ( { } let active = true; - let safety; const reveal = setTimeout( () => { if ( active ) { setPersonalizing( true ); @@ -103,7 +107,7 @@ const SiteList = ( { } }; - safety = setTimeout( done, 9000 ); + const safety = setTimeout( done, 9000 ); get( onboarding.root + '/starter_order?builder=' + @@ -134,13 +138,15 @@ const SiteList = ( { setSearchFailed( false ); const q = ( searchQuery || '' ).trim(); - if ( q.length < 3 ) { + if ( q.length < SEARCH_MIN_CHARS ) { setSearching( false ); return undefined; } let active = true; let safety; + // Lets cleanup abort a superseded in-flight request, not just ignore its result. + const controller = new AbortController(); const done = () => { if ( active ) { clearTimeout( safety ); @@ -166,7 +172,10 @@ const SiteList = ( { '/starter_search?builder=' + encodeURIComponent( editor ) + '&q=' + - encodeURIComponent( q ) + encodeURIComponent( q ), + false, + true, + controller.signal ) .then( ( res ) => { if ( ! active ) { @@ -181,11 +190,18 @@ const SiteList = ( { fail(); } ) - .catch( fail ); - }, 600 ); + .catch( ( err ) => { + // Aborted = superseded, not a real failure → skip the fallback. + if ( err && err.name === 'AbortError' ) { + return; + } + fail(); + } ); + }, SEARCH_DEBOUNCE_MS ); return () => { active = false; + controller.abort(); clearTimeout( timer ); clearTimeout( safety ); }; diff --git a/onboarding/src/utils/rest.js b/onboarding/src/utils/rest.js index beba2da0..a928b100 100644 --- a/onboarding/src/utils/rest.js +++ b/onboarding/src/utils/rest.js @@ -5,8 +5,8 @@ export const send = ( route, data, simple = false ) => { return requestData( route, simple, data ); }; -export const get = ( route, simple = false, useNonce = true ) => { - return requestData( route, simple, {}, 'GET', useNonce ); +export const get = ( route, simple = false, useNonce = true, signal = null ) => { + return requestData( route, simple, {}, 'GET', useNonce, signal ); }; const requestData = async ( @@ -14,7 +14,8 @@ const requestData = async ( simple = false, data = {}, method = 'POST', - useNonce = true + useNonce = true, + signal = null ) => { const options = { method, @@ -37,6 +38,11 @@ const requestData = async ( options.headers[ 'x-wp-nonce' ] = tiobDash.nonce; } + // Optional AbortSignal so callers can cancel an in-flight request. + if ( signal ) { + options.signal = signal; + } + if ( 'POST' === method ) { options.body = JSON.stringify( data ); }