diff --git a/client/src/components/search/WithInstantSearch.js b/client/src/components/search/WithInstantSearch.js
deleted file mode 100644
index c4c4d6ca20..0000000000
--- a/client/src/components/search/WithInstantSearch.js
+++ /dev/null
@@ -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 (
-
- {this.isSearchPage() ? (
-
- ) : (
- <>
-
-
-
-
-
-
- >
- )}
- {this.props.children}
-
- );
- }
-}
-
-InstantSearchRoot.displayName = 'InstantSearchRoot';
-InstantSearchRoot.propTypes = propTypes;
-
-const InstantSearchRootConnected = connect(
- mapStateToProps,
- mapDispatchToProps
-)(InstantSearchRoot);
-
-const WithInstantSearch = ({ children }) => (
-
- {({ location }) => (
-
- {children}
-
- )}
-
-);
-
-WithInstantSearch.displayName = 'WithInstantSearch';
-WithInstantSearch.propTypes = { children: PropTypes.any };
-
-export default WithInstantSearch;
diff --git a/client/src/components/search/searchBar/search-bar.tsx b/client/src/components/search/searchBar/search-bar.tsx
index 8fea9ce5d9..aa60b92dcd 100644
--- a/client/src/components/search/searchBar/search-bar.tsx
+++ b/client/src/components/search/searchBar/search-bar.tsx
@@ -9,8 +9,6 @@ import { AnyAction, bindActionCreators, Dispatch } from 'redux';
import { createSelector } from 'reselect';
import { searchPageUrl } from '../../../utils/algolia-locale-setup';
-import WithInstantSearch from '../WithInstantSearch';
-
import {
isSearchDropdownEnabledSelector,
isSearchBarFocusedSelector,
@@ -18,6 +16,8 @@ import {
toggleSearchFocused,
updateSearchQuery
} from '../redux';
+import WithInstantSearch from '../with-instant-search';
+
import SearchHits from './search-hits';
import './searchbar-base.css';
diff --git a/client/src/components/search/with-instant-search.tsx b/client/src/components/search/with-instant-search.tsx
new file mode 100644
index 0000000000..1455a69096
--- /dev/null
+++ b/client/src/components/search/with-instant-search.tsx
@@ -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();
+ 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();
+ 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 (
+
+ {isSearchPage() ? (
+
+ ) : (
+ <>
+
+
+
+
+
+
+ >
+ )}
+ {children}
+
+ );
+}
+
+InstantSearchRoot.displayName = 'InstantSearchRoot';
+
+const InstantSearchRootConnected = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(InstantSearchRoot);
+
+interface WithInstantSearchProps {
+ children: ReactNode;
+}
+function WithInstantSearch({ children }: WithInstantSearchProps): JSX.Element {
+ return (
+
+ {({ location }) => (
+
+ {children}
+
+ )}
+
+ );
+}
+
+WithInstantSearch.displayName = 'WithInstantSearch';
+
+export default WithInstantSearch;
diff --git a/config/read-env.js b/config/read-env.js
index 392ca3759c..da9f581148 100644
--- a/config/read-env.js
+++ b/config/read-env.js
@@ -46,11 +46,11 @@ module.exports = Object.assign(locations, {
environment: process.env.FREECODECAMP_NODE_ENV || 'development',
algoliaAppId:
!algoliaAppId || algoliaAppId === 'app_id_from_algolia_dashboard'
- ? null
+ ? ''
: algoliaAppId,
algoliaAPIKey:
!algoliaAPIKey || algoliaAPIKey === 'api_key_from_algolia_dashboard'
- ? null
+ ? ''
: algoliaAPIKey,
paypalClientId:
!paypalClientId || paypalClientId === 'id_from_paypal_dashboard'