feat(client): ts-migrate WithInstantSearch.js (#42923)

* rename to TS

* update import

* migrate with-instant-search

* change falsy values to empty string

* update import order

Finish resolving conflict from #41824

* update import order for search-bar

* update setTimeout() type
This commit is contained in:
awu43
2021-08-05 13:00:30 -07:00
committed by GitHub
parent 038ac3e7b9
commit 4134404bfd
4 changed files with 208 additions and 181 deletions

View File

@ -1,177 +0,0 @@
import { Location } from '@reach/router';
import algoliasearch from 'algoliasearch/lite';
import { navigate } from 'gatsby';
import PropTypes from 'prop-types';
import qs from 'query-string';
import React, { Component } from 'react';
import { InstantSearch, Configure } from 'react-instantsearch-dom';
import { connect } from 'react-redux';
import Media from 'react-responsive';
import { createSelector } from 'reselect';
import envData from '../../../../config/env.json';
import { newsIndex } from '../../utils/algolia-locale-setup';
import {
isSearchDropdownEnabledSelector,
searchQuerySelector,
toggleSearchDropdown,
updateSearchQuery
} from './redux';
const { algoliaAppId, algoliaAPIKey } = envData;
const DEBOUNCE_TIME = 100;
// If a key is missing, searches will fail, but the client will still render.
const searchClient =
algoliaAppId && algoliaAPIKey
? algoliasearch(algoliaAppId, algoliaAPIKey)
: { search: () => {} };
const propTypes = {
children: PropTypes.any,
isDropdownEnabled: PropTypes.bool,
location: PropTypes.object.isRequired,
query: PropTypes.string,
toggleSearchDropdown: PropTypes.func.isRequired,
updateSearchQuery: PropTypes.func.isRequired
};
const mapStateToProps = createSelector(
searchQuerySelector,
isSearchDropdownEnabledSelector,
(query, isDropdownEnabled) => ({ query, isDropdownEnabled })
);
const mapDispatchToProps = {
toggleSearchDropdown,
updateSearchQuery
};
const searchStateToUrl = ({ pathname }, query) =>
`${pathname}${query ? `?${qs.stringify({ query })}` : ''}`;
const urlToSearchState = ({ search }) => qs.parse(search.slice(1));
class InstantSearchRoot extends Component {
componentDidMount() {
const { toggleSearchDropdown } = this.props;
toggleSearchDropdown(!this.isSearchPage());
this.setQueryFromURL();
}
componentDidUpdate(prevProps) {
const { location, toggleSearchDropdown, isDropdownEnabled } = this.props;
const enableDropdown = !this.isSearchPage();
if (isDropdownEnabled !== enableDropdown) {
toggleSearchDropdown(enableDropdown);
}
if (location !== prevProps.location) {
const { query, updateSearchQuery } = this.props;
if (this.isSearchPage()) {
if (
location.state &&
typeof location.state === 'object' &&
location.state.hasOwnProperty('query')
) {
updateSearchQuery(location.state.query);
} else if (location.search) {
this.setQueryFromURL();
} else {
navigate(searchStateToUrl(this.props.location, query), {
state: { query },
replace: true
});
}
} else if (query) {
updateSearchQuery('');
}
}
}
isSearchPage = () => this.props.location.pathname.startsWith('/search');
setQueryFromURL = () => {
if (this.isSearchPage()) {
const { updateSearchQuery, location, query } = this.props;
const { query: queryFromURL } = urlToSearchState(location);
if (query !== queryFromURL) {
updateSearchQuery(queryFromURL);
}
}
};
onSearchStateChange = ({ query }) => {
const { updateSearchQuery, query: propsQuery } = this.props;
if (propsQuery === query || typeof query === 'undefined') {
return;
}
updateSearchQuery(query);
this.updateBrowserHistory(query);
};
updateBrowserHistory = query => {
if (this.isSearchPage()) {
clearTimeout(this.debouncedSetState);
this.debouncedSetState = setTimeout(() => {
if (this.isSearchPage()) {
navigate(searchStateToUrl(this.props.location, query), {
state: { query }
});
}
}, DEBOUNCE_TIME);
}
};
render() {
const { query } = this.props;
const MAX_MOBILE_HEIGHT = 768;
return (
<InstantSearch
indexName={newsIndex}
onSearchStateChange={this.onSearchStateChange}
searchClient={searchClient}
searchState={{ query }}
>
{this.isSearchPage() ? (
<Configure hitsPerPage={15} />
) : (
<>
<Media maxHeight={MAX_MOBILE_HEIGHT}>
<Configure hitsPerPage={5} />
</Media>
<Media minHeight={MAX_MOBILE_HEIGHT + 1}>
<Configure hitsPerPage={8} />
</Media>
</>
)}
{this.props.children}
</InstantSearch>
);
}
}
InstantSearchRoot.displayName = 'InstantSearchRoot';
InstantSearchRoot.propTypes = propTypes;
const InstantSearchRootConnected = connect(
mapStateToProps,
mapDispatchToProps
)(InstantSearchRoot);
const WithInstantSearch = ({ children }) => (
<Location>
{({ location }) => (
<InstantSearchRootConnected location={location}>
{children}
</InstantSearchRootConnected>
)}
</Location>
);
WithInstantSearch.displayName = 'WithInstantSearch';
WithInstantSearch.propTypes = { children: PropTypes.any };
export default WithInstantSearch;

View File

@ -9,8 +9,6 @@ import { AnyAction, bindActionCreators, Dispatch } from 'redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { searchPageUrl } from '../../../utils/algolia-locale-setup'; import { searchPageUrl } from '../../../utils/algolia-locale-setup';
import WithInstantSearch from '../WithInstantSearch';
import { import {
isSearchDropdownEnabledSelector, isSearchDropdownEnabledSelector,
isSearchBarFocusedSelector, isSearchBarFocusedSelector,
@ -18,6 +16,8 @@ import {
toggleSearchFocused, toggleSearchFocused,
updateSearchQuery updateSearchQuery
} from '../redux'; } from '../redux';
import WithInstantSearch from '../with-instant-search';
import SearchHits from './search-hits'; import SearchHits from './search-hits';
import './searchbar-base.css'; import './searchbar-base.css';

View File

@ -0,0 +1,204 @@
import { Location } from '@reach/router';
import type { WindowLocation } from '@reach/router';
import algoliasearch from 'algoliasearch/lite';
import { navigate } from 'gatsby';
import qs from 'query-string';
import React, { useEffect, useRef } from 'react';
import type { ReactNode } from 'react';
import { InstantSearch, Configure } from 'react-instantsearch-dom';
import { connect } from 'react-redux';
import Media from 'react-responsive';
import { createSelector } from 'reselect';
import envData from '../../../../config/env.json';
import { newsIndex } from '../../utils/algolia-locale-setup';
import {
isSearchDropdownEnabledSelector,
searchQuerySelector,
toggleSearchDropdown,
updateSearchQuery
} from './redux';
const { algoliaAppId, algoliaAPIKey } = envData;
const DEBOUNCE_TIME = 100;
// If a key is missing, searches will fail, but the client will still render.
const searchClient =
algoliaAppId && algoliaAPIKey
? algoliasearch(algoliaAppId, algoliaAPIKey)
: {
// eslint-disable-next-line @typescript-eslint/no-empty-function
search: () => {}
};
const mapStateToProps = createSelector(
searchQuerySelector,
isSearchDropdownEnabledSelector,
function (
query: string,
isDropdownEnabled: boolean
): { query: string; isDropdownEnabled: boolean } {
return { query, isDropdownEnabled };
}
);
const mapDispatchToProps = {
toggleSearchDropdown,
updateSearchQuery
};
function searchStateToUrl(
{ pathname }: { pathname: string },
query: string
): string {
return `${pathname}${query ? `?${qs.stringify({ query })}` : ''}`;
}
function urlToSearchState({ search }: WindowLocation): { query: string } {
return qs.parse(search.slice(1)) as { query: string };
}
interface InstantSearchRootProps {
isDropdownEnabled: boolean;
location: WindowLocation<{ query: string }>;
query: string;
toggleSearchDropdown: (toggle: boolean) => void;
updateSearchQuery: (query: string) => void;
children: ReactNode;
}
function InstantSearchRoot({
isDropdownEnabled,
location,
query,
toggleSearchDropdown,
updateSearchQuery,
children
}: InstantSearchRootProps) {
function isSearchPage(): boolean {
return location.pathname.startsWith('/search');
}
function setQueryFromURL(): void {
if (isSearchPage()) {
const { query: queryFromURL } = urlToSearchState(location);
if (query !== queryFromURL) {
updateSearchQuery(queryFromURL);
}
}
}
useEffect(() => {
toggleSearchDropdown(!isSearchPage());
setQueryFromURL();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
const prevLocationRef = useRef<InstantSearchRootProps['location']>();
useEffect(() => {
prevLocationRef.current = location;
});
const prevLocation = prevLocationRef.current;
useEffect(() => {
const enableDropdown = !isSearchPage();
if (isDropdownEnabled !== enableDropdown) {
toggleSearchDropdown(enableDropdown);
}
if (location !== prevLocation) {
if (isSearchPage()) {
if (
location.state &&
typeof location.state === 'object' &&
location.state.hasOwnProperty('query')
) {
updateSearchQuery(location.state.query);
} else if (location.search) {
setQueryFromURL();
} else {
void navigate(searchStateToUrl(location, query), {
state: { query },
replace: true
});
}
} else if (query) {
updateSearchQuery('');
}
}
});
const debouncedSetState = useRef<number>();
function updateBrowserHistory(query: string): void {
if (isSearchPage()) {
clearTimeout(debouncedSetState.current);
debouncedSetState.current = window.setTimeout(() => {
if (isSearchPage()) {
void navigate(searchStateToUrl(location, query), {
state: { query }
});
}
}, DEBOUNCE_TIME);
}
}
const propsQuery = query;
function onSearchStateChange({ query }: { query: string | undefined }): void {
if (propsQuery === query || typeof query === 'undefined') {
return;
}
updateSearchQuery(query);
updateBrowserHistory(query);
}
const MAX_MOBILE_HEIGHT = 768;
return (
<InstantSearch
indexName={newsIndex}
onSearchStateChange={onSearchStateChange}
searchClient={searchClient}
searchState={{ query }}
>
{isSearchPage() ? (
<Configure hitsPerPage={15} />
) : (
<>
<Media maxHeight={MAX_MOBILE_HEIGHT}>
<Configure hitsPerPage={5} />
</Media>
<Media minHeight={MAX_MOBILE_HEIGHT + 1}>
<Configure hitsPerPage={8} />
</Media>
</>
)}
{children}
</InstantSearch>
);
}
InstantSearchRoot.displayName = 'InstantSearchRoot';
const InstantSearchRootConnected = connect(
mapStateToProps,
mapDispatchToProps
)(InstantSearchRoot);
interface WithInstantSearchProps {
children: ReactNode;
}
function WithInstantSearch({ children }: WithInstantSearchProps): JSX.Element {
return (
<Location>
{({ location }) => (
<InstantSearchRootConnected
location={location as InstantSearchRootProps['location']}
>
{children}
</InstantSearchRootConnected>
)}
</Location>
);
}
WithInstantSearch.displayName = 'WithInstantSearch';
export default WithInstantSearch;

View File

@ -46,11 +46,11 @@ module.exports = Object.assign(locations, {
environment: process.env.FREECODECAMP_NODE_ENV || 'development', environment: process.env.FREECODECAMP_NODE_ENV || 'development',
algoliaAppId: algoliaAppId:
!algoliaAppId || algoliaAppId === 'app_id_from_algolia_dashboard' !algoliaAppId || algoliaAppId === 'app_id_from_algolia_dashboard'
? null ? ''
: algoliaAppId, : algoliaAppId,
algoliaAPIKey: algoliaAPIKey:
!algoliaAPIKey || algoliaAPIKey === 'api_key_from_algolia_dashboard' !algoliaAPIKey || algoliaAPIKey === 'api_key_from_algolia_dashboard'
? null ? ''
: algoliaAPIKey, : algoliaAPIKey,
paypalClientId: paypalClientId:
!paypalClientId || paypalClientId === 'id_from_paypal_dashboard' !paypalClientId || paypalClientId === 'id_from_paypal_dashboard'