Converting Vanilla JavaScript to React
Making the MixIt app into MixItRemix, a React app, without using Router (Part 2: Building the sidebar navigation and rendering the forms)
It bears repeating that the Router hook would have improved the next portion of the app significantly, though the intention of this conversion was to mimic the original app as much as possible.
Bringing back up our component hierarchy, we see that the header is the home of three of our sidebar options. Only the random cocktail navigation button bypasses the header but it shares extremely similar functionality with the forms for name or main ingredient and the first letter dropdown menu, so I decided that the RandomCocktail
component should be a child of Header
, too.
I’m getting ahead of myself, though. First we need to look at the Sidebar
component or more correctly the SidebarOpen
subcomponent and compare it with our vanilla code.
Our plan for creating the various forms appear in the header was to use a display tag in their selectors of the stylesheet set to none. Then as, the user clicked each sidebar option it would simply make the relevant menu appear. To that end, we gave all of the search forms a className
of search and set their display to none.
.search {height: 25px;display: none;}
Then in our JS, we did the same thing we did with the toggleNav
function. We changed their styling to reflect a display of block, which made the form appear in the header.
cocktailName.addEventListener('click', (e) => {hideForms();nameSearchForm.style.display = 'block';})mainIngredient.addEventListener('click', (e) => {hideForms();ingredientSearchForm.style.display = 'block';})firstLetter.addEventListener('click', () => {drinksListContainer.style.display = '';hideForms();populateLetterDropdown();letterDropdown.style.display = 'block';})letterDropdown.addEventListener('change', (e) => {drinksListContainer.innerHTML = ''drinksListContainer.style.display = 'block'detailContainer.style.width = '50%'fetchRequest(byLetter, e.target.value).then(renderList)})
In one way, the conversion to React was significantly better than this but it came with the same drawback as the sidebar sliding open. In React, each menu form renders or unmounts as the user selects different options. This is obviously better from a resources standpoint as all three menus wouldn’t need to load with the page. Still, the issue of the buttons for the sidebar menu living in the Sidebar
component and needing to interact with the Header
component meant that I had to pass up additional state with a callback toApp
, selectForm
, to store the choice the user made so it could be passed back down to Header
to render the selected form.
const SidebarOpen = ({onFormSelect}) => {const handleOpenFormClick = (e) => {onFormSelect(e);}return (<ul><li><span>Search By:</span></li><li><buttononClick={handleOpenFormClick}id='cocktail-name'name="cocktailName">Cocktail Name</button></li><li><buttononClick={handleOpenFormClick}id='first-letter'name="firstLetter">First Letter of Cocktail</button></li><li><buttononClick={handleOpenFormClick}id='main-ingredient'name="mainIngredient">Drink Ingredient</button></li><li><buttononClick={handleOpenFormClick}id='random-cocktail'name="randomCocktail">Random Cocktail</button></li></ul>)}
I followed that with the Header
passing down the callback to App
to each relevant form as it renders.
const Header = ({onDrinkSelection,onFetchSubmission,toggleNavStatus,selectForm}) => {const handleRenderForm = () => {switch (selectForm) {case "cocktailName":return <NameForm onFetchSubmission={onFetchSubmission}/>;case "firstLetter":return <FirstLetterForm onFetchSubmission={onFetchSubmission}/>;case "mainIngredient":return <MainIngredientForm onFetchSubmission={onFetchSubmission}/>;case "randomCocktail":return <RandomCocktail onDrinkSelection={onDrinkSelection}/>;}}return (<header className={toggleNavStatus ? "main-sidebar-open" : "main" }><h1>Mix It Remix</h1>{handleRenderForm()}</header>)}export default Header;
Obviously, another drawback was that it required many more lines of code in the React version compared with the vanilla JavaScript. I foster no illusions that this will be the case for much larger and more complicated applications and sincerely doubt that it is.
Within each form component, there is a single input form that, upon submission, sends the user input as the argument to the callback function declared in App
. I wanted the fetch request to occur in the submit event handler before the callback was invoked to pessimistically render the list of drinks just as it did in the vanilla version.
const NameForm = ({onFetchSubmission}) => {const [nameForm, setNameForm] = useState("");const handleNameFormChange = (e) => {setNameForm(e.target.value);}const handleNameFormSubmit = (e) => {e.preventDefault();getFormSubmission()}const getFormSubmission = () => {fetch(`https://www.thecocktaildb.com/api/json/v1/1/search.php?s=${nameForm.toLowerCase()}`).then(r => r.json()).then(newSubmission => {onFetchSubmission(newSubmission.drinks)})}
return(<formonSubmit={handleNameFormSubmit}className='search'id='search-name'><inputonChange={handleNameFormChange}type='text'value={nameForm}name='input'placeholder='Enter Drink Name'/><input type='submit'/></form>)}export default NameForm;
However, neither the FirstLetter
nor the RandomCocktail
component had a submit and instead I imported the useEffect
hook to fetch and immediately render the list of drinks or random drink respectively.
const RandomCocktail = ({onDrinkSelection}) => {useEffect(() => {fetch(`https://www.thecocktaildb.com/api/json/v1/1/random.php`).then(r => r.json()).then(newSubmission => {onDrinkSelection(newSubmission.drinks[0])})},[])return (<></>)}export default RandomCocktail;
So we are left with a hierarchy that looks like this.
All we need now is the render the DrinksList
and the DrinkDetails
.
Continued in Part 3: Rendering the selection to the body of the page