Skip to content

Commit 0db349a

Browse files
authored
SearchProvider updates (#124)
- Added minQueryLength and debounceTime props for better configuration - Fixed debounce fetching
1 parent 6f54fd1 commit 0db349a

File tree

4 files changed

+87
-12
lines changed

4 files changed

+87
-12
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-storefront",
3-
"version": "8.14.1",
3+
"version": "8.15.0",
44
"description": "Build and deploy e-commerce progressive web apps (PWAs) in record time.",
55
"module": "./index.js",
66
"license": "Apache-2.0",

src/search/SearchProvider.js

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,28 @@ import React, { useState, useEffect } from 'react'
22
import PropTypes from 'prop-types'
33
import SearchContext from './SearchContext'
44
import _fetch from '../fetch'
5-
import debounce from 'lodash/debounce'
5+
import useDebounce from '../utils/useDebounce'
66
import { fetchLatest, StaleResponseError } from '../utils/fetchLatest'
77
import getAPIURL from '../api/getAPIURL'
88

99
const fetch = fetchLatest(_fetch)
1010

11-
export default function SearchProvider({ children, query, initialGroups, active }) {
11+
export default function SearchProvider({
12+
children,
13+
query,
14+
initialGroups,
15+
active,
16+
minQueryLength,
17+
debounceTime,
18+
}) {
1219
const [state, setState] = useState({
1320
groups: initialGroups,
1421
loading: true,
1522
})
1623

17-
useEffect(() => {
18-
if (active) {
19-
fetchSuggestions(query)
20-
}
21-
}, [active, query])
24+
const debouncedQuery = useDebounce(query, debounceTime)
2225

23-
const fetchSuggestions = debounce(async text => {
26+
const fetchSuggestions = async text => {
2427
try {
2528
setState(state => ({
2629
...state,
@@ -43,10 +46,16 @@ export default function SearchProvider({ children, query, initialGroups, active
4346
}))
4447
}
4548
}
46-
}, 250)
49+
}
50+
51+
useEffect(() => {
52+
if (active && (debouncedQuery.length >= minQueryLength || !debouncedQuery)) {
53+
fetchSuggestions(debouncedQuery)
54+
}
55+
}, [active, debouncedQuery])
4756

4857
const context = {
49-
query,
58+
query: debouncedQuery,
5059
state,
5160
setState,
5261
fetchSuggestions,
@@ -58,4 +67,17 @@ export default function SearchProvider({ children, query, initialGroups, active
5867
SearchProvider.propTypes = {
5968
open: PropTypes.bool,
6069
initialGroups: PropTypes.array,
70+
/**
71+
* Minimum length of search query to fetch. Default is 3
72+
*/
73+
minQueryLength: PropTypes.number,
74+
/**
75+
* Default is 250
76+
*/
77+
debounceTime: PropTypes.number,
78+
}
79+
80+
SearchProvider.defaultProps = {
81+
minQueryLength: 3,
82+
debounceTime: 250,
6183
}

src/utils/useDebounce.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useEffect, useState } from 'react'
2+
3+
export default function useDebounce(value, delay) {
4+
// State and setters for debounced value
5+
const [debouncedValue, setDebouncedValue] = useState(value)
6+
7+
useEffect(
8+
() => {
9+
// Update debounced value after delay
10+
const handler = setTimeout(() => {
11+
setDebouncedValue(value)
12+
}, delay)
13+
14+
// Cancel the timeout if value changes (also on delay change or unmount)
15+
// This is how we prevent debounced value from updating if value is changed ...
16+
// .. within the delay period. Timeout gets cleared and restarted.
17+
return () => {
18+
clearTimeout(handler)
19+
}
20+
},
21+
[value, delay], // Only re-call effect if value or delay changes
22+
)
23+
24+
return debouncedValue
25+
}

test/search/SearchProvider.test.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,42 @@ describe('SearchProvider', () => {
6060
fetchMock.mockResponseOnce(JSON.stringify({ groups: 'test2' }))
6161

6262
await act(async () => {
63-
wrapper.setProps({ query: 'a' })
63+
wrapper.setProps({ query: 'abc' })
6464
await sleep(300) // to trigger debounce
6565
await wrapper.update()
6666
})
6767

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

71+
it('should not fetch suggestions if query length is less than minimum', async () => {
72+
fetchMock.mockResponseOnce(JSON.stringify({ groups: 'test' }))
73+
74+
wrapper = mount(
75+
<SearchProvider query="" active>
76+
<ContextGetter />
77+
</SearchProvider>,
78+
)
79+
80+
await act(async () => {
81+
await sleep(300) // to trigger debounce
82+
await wrapper.update()
83+
})
84+
85+
expect(context.state.groups).toBe('test') // check that suggestions aren't fetched on mount
86+
87+
fetchMock.mockResponseOnce(JSON.stringify({ groups: 'test2' }))
88+
89+
await act(async () => {
90+
// Using a query less than default min length
91+
wrapper.setProps({ query: 'a' })
92+
await sleep(300) // to trigger debounce
93+
await wrapper.update()
94+
})
95+
96+
expect(context.state.groups).toBe('test')
97+
})
98+
7199
it('should catch fetch errors and set loading to false', async () => {
72100
fetchMock.mockRejectOnce(new Error('test error'))
73101

0 commit comments

Comments
 (0)