Reusable JS with React

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 autocomplete dropdown form with vanilla JavaScript and some fancy CSS styling.

If you'd like to read about that experience, I wrote about it here.

React

But there are much, much easier ways to accomplish what I was trying to do. Enter React. 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 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 from earlier to see just how "easy" this would be without React. Now on to the code.

DropdownForm.js

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

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:
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:

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:

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:
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:

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 (fromletters). 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!

17