Reusable JS with React
Tags: javascript, react, css, html
Published: 2021-11-19 21:26:05

If you've worked with JavaScript before, you may be familiar with the idea of building out incredibly robust code just to make one small aspect of page functionality. I know this idea all to well. I had the bright idea to try to create an <strong>autocomplete dropdown</strong> form with vanilla JavaScript and some fancy CSS styling.

If you'd like to read about that experience, I wrote about it [here](https://dev.to/michaelneis/the-javascript-iceberg-19eh).

###React
But there are much, much easier ways to accomplish what I was trying to do. Enter <strong>React</strong>. React is a frontend JavaScript framework (well, technically it's a library) that makes creating user interfaces in JavaScript so much more efficient.

I certainly don't have time to go into the full detail of what React had to offer, so if you are completely new I would suggest checking out their [website](https://reactjs.org/) to get started. But, if you don't have time for that, here's a quick TLDR version.

####Components, Props and State
React allows you to create **Components** containing **JSX**. **JSX** is an HTML-adjacent language for creating elements within JavaScript. React will then **render** that component for you. Components can be imported to each other. If a **Parent Component** imports and renders a **Child Component**, it may pass it data as **Props**. Children components can pass data back up to parent components by using callback functions that are received from those **props**. Each component can also have its own **state** data that can be altered, causing the component to re-render and show that new value of **state**. 

####Hooks and Libraries
React supplies us **hooks**. Hooks are importable functions that allow our components to use (or *hook* into) a variety of cool features in React. For example, using the `useState()` hook, we are able to set the initial state to a variable and create a function to alter it at any point and re-render the component. We are also able to create **custom hooks**, which you will see can be really handy. **Libraries** are community-built databases that house all kinds of useful data to be imported. Many of these are used for styling your components, like one I'll be using today, *styled-components*.

If none of this is making sense to you, I would again suggest reading up on React, since this walkthrough will not be focusing much on the nitty-gritty details of *how* everything works.


###Creating an AutoComplete Dropdown Form
Ok, now here's the fun part. Basically, all we'll be doing is creating two files, a **custom hook** and a **dropdown component**. After that, we will be able to render a dropdown anywhere in our project with a simple import of our **dropdown component**. You may be thinking, "wow importing two whole separate files sounds a little complicated for something you said would be easy." If that's the case, you should definitely read my [blog](https://dev.to/michaelneis/the-javascript-iceberg-19eh) from earlier to see just how "easy" this would be without React. Now on to the code. 

####DropdownForm.js
```Javascript
import { useState } from "react"
import styled from "styled-components"
import useDropFill from "../hooks/useDropFill"


function DropdownForm({array, handleSubmit}){

const [letters, setLetters] = useState('')
const [selector, setSelector] = useState(-1)
const [showDrop, setShowDrop] = useState(true)

const showWords = useDropFill(array, letters, selector, showDrop, 
handleHover, handleDropEvent)

function handleDropEvent(e, showWords){
  if(letters !== ''){
    setShowDrop(true)
    if(e.key === "ArrowDown"){
       if (!showWords) return true
         e.preventDefault()
setSelector(selector >=showWords.props.children.length - 1 ? 0 : 
selector + 1)
       } else if (e.key === "ArrowUp"){
          if (!showWords) return true
          e.preventDefault()
          setSelector(selector <= 0 ? 
showWords.props.children.length - 1 : selector - 1)
            } else if ((e.key === "Enter" || e.type === "click")
&& document.getElementById('selectedWordDiv')){
            e.preventDefault()
            const newTextDiv = 
            document.getElementById('selectedWordDiv')
            setLetters(newTextDiv.innerText)
            setShowDrop(!showDrop)
            setSelector(-1)
            }
        } else {
            setSelector(-1)
        }}

        
    function handleHover(index){
        setSelector(index)
    }

    function handleType(e){
        const typedLetters = e.target.value
        setLetters(typedLetters)
    }
    
    function preventSubmit(e){
        e.preventDefault()
        handleSubmit(letters)
        setLetters('')
    }

    document.addEventListener('click', () =>{
        setSelector(-1)
        setShowDrop(false)
    })

    return(
    <DropForm onKeyDown={(e) => handleDropEvent(e, showWords)}
     onSubmit={preventSubmit}>
          <input type='text' onChange={handleType} value= 
           {letters}/>
               {showWords}
            <button type='submit'>Submit</button>
     </DropForm>
    )
}

export default DropdownForm

const DropForm = styled.form`
    display: inline-block;
    position: relative;
    input {
        font-size: 25px;
    }
    button{
        position: absolute;
        margin-left: 10%;
    }
`
```

####useDropFill.js
```JavaScript
import styled from "styled-components"

function useDropFill(array, input, selector, showDrop, 
handleHover, handleDropEvent){

    if (!showDrop) return false;

    const matchWords = array.filter(word => word.substr(0, 
input.length).toLowerCase().includes(input.toLowerCase()))

    if(input === ''){
        const wordDivs = null
        return wordDivs
    }else if(selector >= 0){
const wordDivs = matchWords.map(word => matchWords.indexOf(word) 
=== selector ? <SelectedDiv onClick=
{(e) => handleDropEvent(e)} key={word} id="selectedWordDiv">
<strong>{word.substr(0, input.length)}</strong>
{word.substr(input.length)}</SelectedDiv> : <DropDiv onClick=
{(e) => handleDropEvent(e)} onMouseEnter={() => 
handleHover(matchWords.indexOf(word))} key={word}><strong>
{word.substr(0, input.length)}</strong>{word.substr(input.length)}
</DropDiv>)

        const allDivs = <ContainDiv>{wordDivs}</ContainDiv>

        return(allDivs) 
    }else{
        const wordDivs = matchWords.map(word => <DropDiv onClick=
{(e) => handleDropEvent(e)} onMouseEnter={() => 
handleHover(matchWords.indexOf(word))} key={word}><strong>
{word.substr(0, input.length)}</strong>{word.substr(input.length)}
</DropDiv>)

        const allDivs = <ContainDiv>{wordDivs}</ContainDiv>
        
        return(allDivs)   
    }
}

export default useDropFill

const DropDiv = styled.div`
    border: 1px solid;
    text-align: left;
    background-color: white;
    cursor: pointer;
`
const SelectedDiv = styled.div`
    border: 1px solid;
    text-align: center;
    background-color: #63f0f7;
    font-size: 130%;
    cursor: pointer;
`

const ContainDiv = styled.div`
    position: absolute;
    width: 100%;
`
```

Here we have our component called `DropdownForm` and our custom hook called `useDropFill`. The first thing I would like to point out is the imports that are happening on both. For `DropdownForm` we have a couple. As I mentioned before, `useState` is a React hook that helps us alter state within our component. Both files are importing `styled-components`, a library (we have to install it prior to importing) that lets us add CSS styling within our components. If you look at the bottom of each file, under the export, you will see a few variables being defined as `styled.(something)`. The variable will become whatever you put in 'something', followed by its styling within the backticks. For example, our `ContainDiv` from the bottom of our hook file will create a div called ContainDiv with an absolute position and width of 100% that can be used within the component. You will see these variables appear in the code similarly to imported components, but don't let that confuse you too much, at the core, `ContainDiv` is just a div. These styling variables will come in handy later.

#####Let's map out what the end goal is here:
```JavaScript
function DropdownForm({array, handleSubmit}){

//A bunch of code here...

return(
   //A <form> with special features
)
}

export default DropdownForm
```
Basically, what we want to do is be able to import our **DropdownForm** into other components of our application in order to use this dropdown multiple times. As you can see, we are passing in **props** of `array` and `handleSubmit`. The `array` will be whatever array of strings we want to have the dropdown populate with, and the `handeSubmit` will be a callback function used to handle the text that is submitted in the form. Let's say you wanted to make a dropdown form for certain fruits and console log the fruit that is submitted. All you would have to do is:
```JavaScript

import DropdownForm from "./DropdownForm"

function Fruits(){

    const fruitArray = ["lemon", "orange", "apple", "banana"]

    function handleSubmit(text){
        console.log(text)
    }

    return(
        <DropdownForm array={fruitArray} handleSubmit = 
         {handleSubmit}/>
    )
}
```
Pretty cool right? And the best part is you can do this multiple times in multiple different components. Pass it an array of your choosing and handle the results however you want. That's why React is so handy, it allows for dynamic reusability of components and hooks. One very **important note** is that in order for the `DropdownForm` to render properly, it must recieve *both* an array and handleSubmit prop, and the array can only include **strings**. Otherwise, you have free reign to use it as you please.

Now let's take a more detailed look at what is going on in `Dropdownform`:
```JavaScript

const [letters, setLetters] = useState('')
const [selector, setSelector] = useState(-1)
const [showDrop, setShowDrop] = useState(true)

const showWords = useDropFill(array, letters, selector, showDrop,
handleHover, handleDropEvent)

//handleDropEvent and hanldeHover go here

function handleType(e){
        const typedLetters = e.target.value
        setLetters(typedLetters)
    }
    
    function preventSubmit(e){
        e.preventDefault()
        handleSubmit(letters)
        setLetters('')
    }

    document.addEventListener('click', () =>{
        setSelector(-1)
        setShowDrop(false)
    })

return(
    <DropForm onKeyDown={(e) => handleDropEvent(e, showWords)} 
     onSubmit={preventSubmit}>
          <input type='text' onChange={handleType} value=
          {letters}/>
              {showWords}
           <button type='submit'>Submit</button>
     </DropForm>
    )
```
We are creating three *stateful* variables (letters, selector, and showDrop) and one const `showWords` which will equal the return of our custom hook `useDropFill`. We will pass all of our stateful variables into the hook as well as our original *array* and a few callback functions, `hanldeDropEvent` and `handleHover` (we'll get into those in a bit). On our return, we create a form (remember, we're using `styled-components` here, so `DropForm`, for all intents and purposes, is just a `<form>`). That form includes an input and a submit button. The value (or text input) of our input is set to `letters`, so it will start at that initial state of ''. In order to change the value to what the user is typing, we add an onChange event which is passed the function `handleType`. All this function does is take the value of what is being typed (or *changed*) and sets `letters` to that value (with useState). Now we are able to target the value of the input by targeting `letters`, which is why we pass it into our hook. Our form also has an `onKeyDown` event which listens on the whole form to whenever a key is pressed, which we pass the callback function `handleDropEvent` (again, more on that one later). The `onSubmit` event passes the form submission to the callback `preventSubmit`, which will *prevent* the default of reloading the page. This function also takes the value of `letters` and passes it to our `handleSubmit` callback function. This is what is being returned to the parent component (in the *fruits* example, this would be the **text** that we are console logging). Next, we've created an eventlistener for the entire document. This will listen for a click anywhere, set `selector` back to its original state of -1 and set `showDrop` to false. `showDrop` will become more clear once we take a look at our hook, but its functionality is that when `showDrop` is *true*, we will see a dropdown, and when it is *false*, we won't. Finally, we pass our `showWords` const within our form, which will either be a *false* value or a div containing all of our matching words, depending on what happens in our hook. Now let's look at what `selector` is and why we need it. 

#####Here are the two callback functions from earlier:
```JavaScript
 function handleDropEvent(e, showWords){
        setShowDrop(true)
        if(letters !== ''){
            if(e.key === "ArrowDown"){
                if (!showWords) return true
                e.preventDefault()
                setSelector(selector >= 
showWords.props.children.length - 1 ? 0 : selector + 1)
            } else if (e.key === "ArrowUp"){
                if (!showWords) return true
                e.preventDefault()
                setSelector(selector <= 0 ? 
showWords.props.children.length - 1 : selector - 1)
            } else if ((e.key === "Enter" || e.type === "click") 
&& document.getElementById('selectedWordDiv')){
                e.preventDefault()
                const newTextDiv = 
document.getElementById('selectedWordDiv')
                setLetters(newTextDiv.innerText)
                setShowDrop(!showDrop)
                setSelector(-1)
            }
        } else {
            setSelector(-1)
        }}


function handleHover(index){
    setSelector(index)
 }
```
Both of these functions deal with our `selector` variable, which starts at -1 and will always be a number. Our `handleHover` function simply sets `selector` to an index. That index will be returned from our hook. The `handleDropEvent` is the function that we are calling back to on the form `keyDown` event. We pass it e (the event) and showWords. The first thing it does is set `showDrop` to true, because on any keydown event in our form, we want to see the possible words that match. We then have a blanket if statement that checks whether or not `letters` is an empty string. If it is (if the input is empty), all we need to do is reset our selector to -1. If it is not (if anything has been typed), then we want to be able to manipulate our selector. Here, we have three conditionals for specific keyboard keys using `e.key`. The three keys we want to check are the up and down arrows, and the enter key. If none of these keys have been pressed, the function does nothing else. What we want to do next is set our selector to a value that will highlight a specific word in our dropdown, with down arrow increasing (scrolling down) and up arrow decreasing (scrolling up) that value. Since our `showWords` will be an div with child divs, we can treat the children as an array for `selector` to match the index of. We also want to make sure that `selector` never goes below zero or becomes a higher number than the number of child divs being shown. So, we can add a ternary in our `setSelector` so that it will loop back to the largest number index of the array if it goes below zero, and loop back to zero if it surpasses that largest index. Now the value of `selector` will stay within the range of child divs. The preventDefault in both of the arrow keys is optional, all it does is stop the cursor from moving in the text input. We also have an if statement in both to prevent the selector from changing if there are no words to show. The enter key is a little different. Once we look at our custom hook, you will see that we will be assigning our selected word div an id of "selectedWordDiv", which will help with our styling but also help here. We also give the child divs click events that will call back to this function, which is why we include the or statement. We want everything here to happen if the enter key is pressed OR the div is clicked. The && statement makes sure that a div with our special id exists before performing the rest of the function. Prevent default here stops the form from being submitted (since enter is usually used for submitting forms). Now we are setting our `letters` to whatever text that div contained, falsifying (hiding) our showDiv, and setting our `selector` back to -1. Now, whatever word from our string we clicked on will populate the text box. Pretty cool, right? Now we can submit the form with that word (or words, depending on how many were in that index of the array) and our handleSubmit will receive it as an argument.

Let's have a look at how we created those dropdown divs in our `useDropFill` hook:
```JavaScript
function useDropFill(array, input, selector, showDrop, 
handleHover, handleDropEvent){

    if (!showDrop) return false;

    const matchWords = array.filter(word => word.substr(0, 
input.length).toLowerCase().includes(input.toLowerCase()))

    if(input === ''){
        const wordDivs = null
        return wordDivs
    }else if(selector >= 0){
        const wordDivs = matchWords.map(word => 
matchWords.indexOf(word) === selector ? <SelectedDiv onClick={(e)
=> handleDropEvent(e)} key={word} id="selectedWordDiv"><strong>
{word.substr(0, input.length)}</strong>{word.substr(input.length)}
</SelectedDiv> : <DropDiv onClick={(e) => handleDropEvent(e)} 
onMouseEnter={() => handleHover(matchWords.indexOf(word))} key=
{word}><strong>{word.substr(0, input.length)}</strong>
{word.substr(input.length)}</DropDiv>)

        const allDivs = <ContainDiv>{wordDivs}</ContainDiv>

        return(allDivs) 
    }else{
        const wordDivs = matchWords.map(word => <DropDiv onClick=
{(e) => handleDropEvent(e)} onMouseEnter={() => 
handleHover(matchWords.indexOf(word))} key={word}><strong>
{word.substr(0, input.length)}</strong>{word.substr(input.length)}
</DropDiv>)

        const allDivs = <ContainDiv>{wordDivs}</ContainDiv>
        
        return(allDivs)   
    }
}
```
That looks like a big bundle, but lets take it step by step. Keep in mind here, the `input` that we are passing here will equal whatever letters we have typed (from`letters`). First, as mentioned before, if `showDrop` is false, this hook just returns false and is done. Otherwise, we create a const `matchWords` that will filter our array. We are filtering each string in our array based on a substr, which will only take the same amount of letters from our word as there are letters in our input. That way when a user types, they will only see words that start with the letters that they have typed. If our input is empty, we will return a null value and be done. Skipping down a bit, if our selector hasn't changed yet (is < 0) we will map out our divs based on the matching words, create a container div for all of those, and return the container div. If the selector is >= 0, we map out divs for `matchWords` like before, but this time on a ternary. If the index of the word in the array matches `selector`, we give it that special "selectedWordDiv" id from before so that it can be selected. Each div has the onclick event, as previously mentioned, but also an onMouseEnter event, which will callback to `handleHover` and give it the index of the current word, setting `selector` to whatever div the user's mouse is hovering over. The strong tags are not necessary, but are used here to make the matching letters **bold**.

And that's it! In terms of styling, all of the forms that you render will have the same styling aspects, so if you want to change one, you will have to change them all. The form must have a position of *relative*, and the div container must have a position of *absolute*. Otherwise, the styling is up to you!