RFR Section 9 – Search Overlay


React

Updated Mar 14th, 2022

Table of Contents

Course Summary

Chapter Details

Setting Up Search Overlay

Create new “Search.js” Component and import into “Main.js” file. Just below footer add in a self closing Search tag. Copy and paste in html in search overlay div from search is visible on course github. Update class to “className” and for to “htmlFor” and “autoFocus” and “autoComplete.”

Display conditionally by adding to “app wide state” a property named “isSearchOpen” and initially set to false.

const initialState = {
  isSearchOpen: false
}

// in reducer

case "openSearch"
draft.isSearchOpen = true
return

case "closeSearch"
draft.isSearchOpen = false
return

Add two cases to your reducer one for openSearch that sets draft.isSearchOpen equal to true and one for closeSearch that sets draft.isSearchOpen equal to false. In the JSX in the “Main.js” file, use curly brackets and ternary operator to conditionally show search component.

{state.isSearchOpen ? <Search /> : ''}

In the “headerLoggedIn.js” Component on the link that contains the search icon add an “onClick” prop that is set to curly brackets and inside have a function named “handleSearchIcon” and create a function with that name that prevents the default and then calls app dispatch give it an object that has a type of openSearch.

function handleSearchIcon(e) {
  e.preventDefault()
  appDispatch({type: "openSearch"})
}

Now go to the “Search.js” component, bring in app-wide-dispatch, and on the link that closes the search overlay add an “onClick” prop set equal to curly brackets and inside the early brackets have a inline function that dispatches the “closeSearch” action.

<span onClick={()=> appDispatch({type: "closeSearch"})}>
  <i className="fas fa-times-circle"></i>
</span>

This works but we want to also be able to close the search using the escape key. Within the “Search.js” component, and within the component function, after “const appDispatch” line, have a “useEffect” with an empty dependency array and give it an arrow function. After the dependency array but still within the search function, add a function called “searchKeyPressHandler.”

useEffect(() => {

  // doing things the non-react way
  // but the classic web-browserish way
  // requires clean-up
  document.addEventListener("keyup", searchKeyPressHandler)

  return () => {
    document.removeEventListener("keyup", searchKeyPressHandler)
  }  
}, [])

function searchKeyPressHandler(e) {
  if ( e.keyCode == 27) {
    appDispatch({type: 'closeSearch'})
  }
}

Note: “e.keyCode” has been deprecated but the new implementation is very similar/simple.

React Transition Group

When you want something to animate instead of being abrupt in React you need a package called “react transition group.” The reason this is needed Is the search overlay doesn’t exist in the DOM yet because it hasn’t been clicked because we wrote a ternary operator that says it’s only going to be shown when the open search property is set to true. You can’t transition an element that doesn’t exist yet in the DOM. And the millisecond you hit the close icon it is unmounted from the DOM. So you can’t transition elements that don’t exist or did exist but no longer exist.

Use “npm install react-transition-group” to get around this issue. Just in the last week, “react-transition-group” has been downloaded 5.7 million times, (7.3 million as of 3/7/22). Import this into “Main.js” and down in the JSX get rid of the search line and add in a CSSTransition tags.

import {CSSTransition} from "react-transition-group"

In the opening tag give it a “timeout” equal curly brackets and inside the brackets give it the duration that matches your CSS animation duration. Also give a prop of “in” and set it to {state.isSearchOpen} give it another prop called “classNames” and note that “classNames” is plural and set it equal to “search-overlay” which is the name that matches the CSS classes. Give it one more prop of “unmountOnExit” which is our way of saying when the in property is false we don’t want it to be invisible or hidden we want to completely remove it from the DOM.

<CSSTransition timeout={330} in={state.isSearchOpen} classNames={"search-overlay"} unmountOnExit>
  <Search />
</CSSTransition>

The best way to try to understand what CSS transition is doing is to look at your CSS file. And in this case look for your “search-overlay” class and you will see below that you have “search-overlay-enter” and “search-overlay-enter-active” and “search-overlay-exit” and “search-overlay-exit-active” classes and CSS transition group is smart enough add these classes at just the right moment.

Add a tool tip to the search icon by going to “headerloggedIn.js” file and, after importing, below the anchor element that is the search icon add a react tool tip self-closing component and give it a place prop.

Make sure it’s at the bottom give it an ID of search give it a class name set to custom-tool tip. Associated the tooltip with the item itself by adding to the anchor element a “data-for” and match the ID you gave to the tooltip and give the a element a “data-tip” attribute. Import react tool tip from react tool tip. While you’re here do the same thing for the chat icon and profile avatar.

Add a little more horizontal spacing because of the React spacing issue that’s been discussed earlier. Need to use the “curly-brackets-with-space-in-quotes” trick.

Article regarding the index in the next lesson

In the next video at around the 14 to 15 minute mark we go into MongoDB Atlas to create an index for our post collection. You do not need to create an index if you downloaded the backend-API files for our project after March 15th, 2021. Essentially there was a line added to the “models/Post.js” file in our backend that will create the index for us automatically.

Waiting for User to Stop Typing

To avoid overloading the server, send requests after a short delay (700ms).

This chapter introduces a third way of working with state that is not “useState” or “useImmerReducer.”

“useImmer” is similar to React’s “useState,” but let’s you have multiple properties instead of say five different “useStates.” You can update one of the properties in an immutable fashion. Leverages “draft” to mutate.

const [state, setState] = useImmer({
  searchTerm: '',
  results: [],
  show: 'neither',
  requestCount: 0
})

Add an “onChange” prop to the input field that calls a custom function called “handleInput().”

function handleInput(e) {
  const value = e.target.value
  setState(draft => {
    draft.searchTerm = value
  })
}

Send request to the backend for every time the search term changes. Perfect case for “useEffect” on “state.searchTerm” but don’t want a request for every keystroke. Set a constant variable named delay to setTimeout(). Run a cleanup function with a clearTimeout(). In the timeout function’s function, use “setState()” and “draft” to increment the “requestCount.”

useEffect(() => {
  const delay = setTimeout(() => {
    setState(draft => {
      draft.requestCount++
    })
  }, 700)

  return () => clearTimeout(delay)
}, [state.searchTerm])

Note that a cleanup function runs on unmount but also when the function runs again. This is great for the “clearTimeout.”

Add another “useEffect” to check if “requestCount” is greater than zero and, if so, then send “axios” request.

useEffect(() => {
  if (state.requestCount) {
  // send axios request here
  }
}, [state.requestCount])

In MongoDB Atlas, set up a new text index in your “posts” collection for the “title” and “body” fields.

Note that this step is unnecessary based on the note in the previous chapter.

Finishing Search Part 1

Import Axios and then send an http request in a “useEffect”

useEffect(() => {
  if (state.requestCount) {
    const ourRequest = Axios.CancelToken.source()
    async function fetchResults() {
      try {
        const response = await Axios.post('/search', {searchTerm: state.searchTerm}, {cancelToken: ourRequest.token})
        // console.log(response.data)
        
      } catch (e) {
        console.log("There was a problem or request cancelled")
      }
    }
    fetchResults()
    return () => ourRequest.cancel()
  }
}, [state.requestCount])

Have animated loading icon in JSX just inside the “container” div and above the “live-search-results” div.

Conditionally show a “circle-loader–visible” class and “live-search-results–visible” class with two ternary operators utilizing the “state.show” property. Note the space inside the quotes.

<div className={"circle-loader " + (state.show == "loading" ? 'circle-loader--visible' : '')}
<div className={"live-search-results " + (state.show == "results" ? 'live-search-results--visible' : '')}

In our initial state, we set “show” property to equal “neither.” Now we want to update this property to either be “loading” or “results” at just the right moment. Go up to the “useEffect” where we set up the delay and tweak to handle spaces with “trim()” and if no search field then we set to neither, (to handle clear).

useEffect(() => {
  if (state.searchTerm.trim()) {
    setState((draft) => {
      draft.show = "loading"
    })
    const delay = setTimeout(() => {
    setState(draft => {
      draft.requestCount++
    })
  }, 700)

  return () => clearTimeout(delay)
  } else {
    setState((draft) => {
      draft.show= "neither"
    })
  }
}, [state.searchTerm])

Note that the “.trim()” method is to handle the spacebar.

In the “axios” request use “setState” to set “show” to “results.”

Finishing Search Part Two

We want to loop through the “results” property and output real data from the server .

For dynamic text in the blue header area that says how many items were found:

<strong>Search Results</strong>({state.results.length} {state.results.length > 1 ? "items" : "item"} found)

Delete all three hard-coded posts. Output posts with “state.results.map()” and give it an arrow function.

state.results.map((post) => {
  // borrowed JSX here from "profile-posts.js"
  // adjust by author name to be dynamic
  // { post.author.username }
})

Make sure this file has imported the “Link” component from “react-router-dom” but the necessary anchor tags should already be updated.

Automatically close search overlay when user clicks a post title link by adding an “onClick” prop to the opening Link tag.

<Link onClick={() => {appDispatch(type: "closeSearch")}}>

Handle the page redirected to the post the user selected by updating the dependency array in the “viewSinglePost” file in the “useEffect” where the request is, to be “id”

Handle the situation in which no search results are found by wrapping the “map” function in conditional logic and if there were no posts found then show the user a message with “alert alert-danger” classes.

{Boolean(state.results.length) && (
  // paste in map function code
)}

{!Boolean(state.results.length) && <p className="alert alert-danger text-center shadow-sm">No results found for that search.</p>
}

Note that we use “Boolean” instead of using “state” directly so you don’t show the zero.

Also note the parenthesis used after the double ampersand is needed because we are dropping down to a new line. Keeping the code on one line removes the need for the parenthesis.

Last detail is to reduce the delay in the “useEffect” function from 3000ms (initially set higher for development testing) to 750ms.