Skip to content

Commit

Permalink
SearchProvider updates (#124)
Browse files Browse the repository at this point in the history
- Added minQueryLength and debounceTime props for better configuration
- Fixed debounce fetching
  • Loading branch information
dijs authored Aug 4, 2020
1 parent 6f54fd1 commit 0db349a
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 12 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-storefront",
"version": "8.14.1",
"version": "8.15.0",
"description": "Build and deploy e-commerce progressive web apps (PWAs) in record time.",
"module": "./index.js",
"license": "Apache-2.0",
Expand Down
42 changes: 32 additions & 10 deletions src/search/SearchProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,28 @@ import React, { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import SearchContext from './SearchContext'
import _fetch from '../fetch'
import debounce from 'lodash/debounce'
import useDebounce from '../utils/useDebounce'
import { fetchLatest, StaleResponseError } from '../utils/fetchLatest'
import getAPIURL from '../api/getAPIURL'

const fetch = fetchLatest(_fetch)

export default function SearchProvider({ children, query, initialGroups, active }) {
export default function SearchProvider({
children,
query,
initialGroups,
active,
minQueryLength,
debounceTime,
}) {
const [state, setState] = useState({
groups: initialGroups,
loading: true,
})

useEffect(() => {
if (active) {
fetchSuggestions(query)
}
}, [active, query])
const debouncedQuery = useDebounce(query, debounceTime)

const fetchSuggestions = debounce(async text => {
const fetchSuggestions = async text => {
try {
setState(state => ({
...state,
Expand All @@ -43,10 +46,16 @@ export default function SearchProvider({ children, query, initialGroups, active
}))
}
}
}, 250)
}

useEffect(() => {
if (active && (debouncedQuery.length >= minQueryLength || !debouncedQuery)) {
fetchSuggestions(debouncedQuery)
}
}, [active, debouncedQuery])

const context = {
query,
query: debouncedQuery,
state,
setState,
fetchSuggestions,
Expand All @@ -58,4 +67,17 @@ export default function SearchProvider({ children, query, initialGroups, active
SearchProvider.propTypes = {
open: PropTypes.bool,
initialGroups: PropTypes.array,
/**
* Minimum length of search query to fetch. Default is 3
*/
minQueryLength: PropTypes.number,
/**
* Default is 250
*/
debounceTime: PropTypes.number,
}

SearchProvider.defaultProps = {
minQueryLength: 3,
debounceTime: 250,
}
25 changes: 25 additions & 0 deletions src/utils/useDebounce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useEffect, useState } from 'react'

export default function useDebounce(value, delay) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value)

useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)

// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler)
}
},
[value, delay], // Only re-call effect if value or delay changes
)

return debouncedValue
}
30 changes: 29 additions & 1 deletion test/search/SearchProvider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,42 @@ describe('SearchProvider', () => {
fetchMock.mockResponseOnce(JSON.stringify({ groups: 'test2' }))

await act(async () => {
wrapper.setProps({ query: 'a' })
wrapper.setProps({ query: 'abc' })
await sleep(300) // to trigger debounce
await wrapper.update()
})

expect(context.state.groups).toBe('test2') // check that suggestions are fetched only after active is set to true
})

it('should not fetch suggestions if query length is less than minimum', async () => {
fetchMock.mockResponseOnce(JSON.stringify({ groups: 'test' }))

wrapper = mount(
<SearchProvider query="" active>
<ContextGetter />
</SearchProvider>,
)

await act(async () => {
await sleep(300) // to trigger debounce
await wrapper.update()
})

expect(context.state.groups).toBe('test') // check that suggestions aren't fetched on mount

fetchMock.mockResponseOnce(JSON.stringify({ groups: 'test2' }))

await act(async () => {
// Using a query less than default min length
wrapper.setProps({ query: 'a' })
await sleep(300) // to trigger debounce
await wrapper.update()
})

expect(context.state.groups).toBe('test')
})

it('should catch fetch errors and set loading to false', async () => {
fetchMock.mockRejectOnce(new Error('test error'))

Expand Down

0 comments on commit 0db349a

Please sign in to comment.