How to Build an Online Dictionary Using React

Full guided project to learn React.js

React.js is ever-growing in popularity and it's better not to fall behind. Let's dive into the basics and complexities of React.js and how we can use it to build a dictionary app for ourselves!

PS: You need to know the very basics of React.js like its component-based rendering, basic lifecycle methods and React hooks. Check out Arjun Singh's blog series on React for beginners to learn basic React, if you haven't yet.

Project Preview

The completed project mightn't look the same for you (if you use different styling) but will function as follows:

  1. Landing page - notice the "toggle theme" button in the bottom-right corner.

    This button changes the theme of the page from light to dark. Toy with it on my hosted version.

  2. Interface after entering a word in and pressing 'search' - a momentary loading text shows up and then a bunch of meanings show up of that word, in different use cases (like as a verb or noun, etc). Also, for some words, pronunciation buttons will show up.

Setup

Check out my GitHub repository for a look into the folder structure. Once you've understood that, set up a React project using npx create-react-app or using Vite.js. I love to use Vite as it simplifies the process of setup much more and is lightning quick as compared to the traditional method of npx create-react-app.

Once that's done, copy the file main.jsx from my Github repo as I have linked previously.

App.jsx

Create a file called App.jsx and another file called App.css. The CSS file would be containing our styles. I won't be discussing the CSS part, you can just copy-paste that from my repo. The main aim of this blog is to explain React.js.

import { createContext, useState } from 'react'
import './App.css'

function App() {


  return (
      <div className='app container-fluid light'>

      </div>
  )
}

export default App

We'll be using the createContext hook and useState hook later in the project.

Search Component

Here's the code for search.jsx. Also, don't forget to create a file called search.css for the CSS files. Paste the following code:

import './search.css';

const Search = (props) => {
    // handleSubmit function to pass data from child to parent
    const {handleSubmit} = props;

    const [search, setSearch] = useState('');

    // Function to handle change in input box - updating the state
    const handleChange = (e) => {
        setSearch(e.target.value);
    }

    const handleSearchSubmit = (e) => {
        handleSubmit(e, search);
        setSearch('');
    }

    return (
        <div className='search d-flex justify-content-center'>
            <form className='form' onSubmit={handleSearchSubmit}>
                <input type="text" className='search-input rounded light' name="search" id="search" value={search} onChange={handleChange} placeholder='Type in a word...' />
                <button type="submit" className='search-button rounded light' disabled={search.trim()===''}><i className="fa-solid fa-magnifying-glass"></i> Search this word</button>
            </form>
        </div>
    )
}

The code is starting to get interesting now. Here's what's going on:

  1. We have a function that we're importing from the props passed into this component called handleSubmit. We also have a state called search, which holds the value of the text the user types into the box.

  2. There are two functions we've defined inside this component, called handleChange and handleSearchSubmit. In handleChange we're updating the value of search using setSearch once the user modifies the text and in handleSearchSubmit we're calling the handleSubmit function received from the props and passing in the event and the search text into it. This is the way by which we transfer data from the child component to its parent. We'll be defining handleSubmit in our App.jsx file in a bit.

Modified App.jsx to render the search component:

import { createContext, useState } from 'react'
import Search from "./search";
import './App.css'

function App() {

  const handleSearchSubmit = (e, searchValue) => {
    // Prevent default form submit behaviour
    e.preventDefault();
    console.log(searchValue)
  }

  return (
      <div className='app container-fluid light'>
        <Search handleSubmit={handleSearchSubmit} />

      </div>
  )
}

export default App

This code will just log the text that we type into the search box in the console. The actual dictionary part is up next.

Please note

After each component's code, I mightn't provide the code for importing it into its parent component. If you are unaware of how to do that yourself, check out my Github repo and go through the files.

API Calls and Loading State

import { createContext, useState } from 'react'
import Search from "./search";
import './App.css'

function App() {

  const [loading, setLoading] = useState(false);
  const [msg, setMsg] = useState('Loading...')

  const [currentWord, setCurrentWord] = useState({word: '', phonetic: '', meanings: []});

  const handleSearchSubmit = (e, searchValue) => {
    // Prevent default form submit behaviour
    e.preventDefault();
    setLoading(true)

    async function getWordData() {
      const res = await fetch(
        `https://api.dictionaryapi.dev/api/v2/entries/en/${searchValue}`
      );
      const data = await res.json();
      console.log(data);
      if (data) {
        if (data.message) {
          setMsg(data.message);
          return
        } else {
          setLoading(false);
        }
      }

      setCurrentWord({
        word: data[0].word,
        phonetic: data[0].phonetics[0].audio,
        meanings: data[0].meanings
      });
    }
    getWordData();
  }

  return (
      <div className='app container-fluid light'>
        <Search handleSubmit={handleSearchSubmit} />
        {loading && <div className="loading p-2">{msg}</div>}
        {!loading &&
          currentWord.word != '' ? <Word wordDescription={currentWord} /> : null
        }
      </div>
  )
}

export default App

You might've noticed we've added an API call inside of our function getWordData. Once it's converted into JSON, we update the values of the currentWord state and add in the word, it's pronunciation audio file and meanings into the object.

Also, notice the code we've added to indicate a loading state. Right before making the API Call, which generally takes about 1-2 seconds, we're changing loading state to true and the JSX inside our return statement will display a text saying loading once loading equals true. Once we receive the data, we toggle loading, and the data of the word that we've received gets displayed via the Word component. Let's create this component now!

Word Component

Add the following code into a new file called Word.jsx.

import './word.css';
import Meaning from "./meaning";

const Word = (props) => {
    const {wordDescription} = props;
    const audioElement = new Audio(wordDescription.phonetic);

    const playAudio = () => {
        audioElement.play();
    }

    return (
        <div className='word d-flex justify-content-center flex-column'>
            <div className='w mb-1'>{wordDescription.word}</div>
            {wordDescription.phonetic != '' ? 
                <button onClick={playAudio} className='p-2 audio-btn light'><i class="fa-solid fa-ear-listen"></i> Pronounce this word</button> : null
            }
            <div className='meanings'>
                {
                    wordDescription.meanings.map((meaning, index)=>(
                        <Meaning id={index} data={meaning} />
                    ))
                }
            </div>
        </div>
    )
}

export default Word

The data that we pass into the props of the Word component are used in this code. We're adding an event listener to the "Pronounce this word" button and playing the audio once it's pressed. The audio is just a link to a file sent by the API. Also, we're checking if there's a link or not using the ternary operator in the JSX.

We're mapping over the array wordDescription.meanings, and for every meaning present, we're rendering a component called Meaning and passing the necessary data as its props. Let's make this component now...

Meaning Component

Here's the code required to put inside meaning.jsx:

import './meaning.css';

const Meaning = (props) => {
    const {id, data} = props;
    const makeStr = (arr, arg) => {
        let newStr = '';
        if (arg == 'syn') {
            newStr += 'Synonyms - '
        } else {
            newStr += 'Antonyms - '
        }
        if (arr.length == 0) {return}
        if (arr.length == 1) {return arr[0]}
        for (let i = 0; i < arr.length; i++) {
            let word = arr[i];
            newStr+=arr[i];
            if (arr.length-1 != i) {
                newStr+=', '
            }
        }
        return newStr;
    }

    return (
        <div className='meaning rounded p-1 mb-3 light' key={id}>
            <div className='part-speech'>as a {data.partOfSpeech}</div>
            <div className='definitions'>
                <ol>
                    {
                        data.definitions.map((def, index)=>(
                            <p key={index+100}>{def.definition}</p>
                        ))
                    }
                </ol>
            </div>
            <div className='synonyms'>
                {
                    makeStr(data.synonyms, 'syn')
                }
            </div>
            <div className='antonyms'>
                {
                    makeStr(data.antonyms, 'ant')
                }
            </div>
        </div>
    )
}

export default Meaning;

That's a pretty straightforward React component. We're checking if there are antonyms and synonyms for the word as well as displaying it if it's present. We're mapping the definitions using data.definitions.map(). We've got data and id from the props passed to this component.

Toggle Theme

We're over with the meat of the app! You might've noticed though that we have a theme-changing (light to dark and vice-versa) button as well on my dictionary web app. To build this feature, understanding the useContext() react hook is crucial. Let's get into it.

Here's what the official React.js website has to say about context in React apps:

Usually, you will pass information from a parent component to a child component via props. But passing props can become verbose and inconvenient if you have to pass them through many components in the middle, or if many components in your app need the same information. Context lets the parent component make some information available to any component in the tree below it—no matter how deep—without passing it explicitly through props.

How do we make the information available without explicitly passing it through the props? We can just export the context from our App.jsx file and then import it inside all of our components. This is a much better alternative than using props.

Let's start by modifying the code in App.jsx:

import { createContext, useState } from 'react'
import Search from "./search";
import Word from './word';
import Toggle from './toggle';
import './App.css'

export const ThemeContext = createContext(null);

function App() {
  const [theme, setTheme] = useState(false);

  const [loading, setLoading] = useState(false);
  const [msg, setMsg] = useState('Loading...')

  const [currentWord, setCurrentWord] = useState({word: '', phonetic: '', meanings: []});

  const handleSearchSubmit = (e, searchValue) => {
    // Prevent default form submit behaviour
    e.preventDefault();
    setLoading(true)

    async function getWordData() {
      const res = await fetch(
        `https://api.dictionaryapi.dev/api/v2/entries/en/${searchValue}`
      );
      const data = await res.json();
      console.log(data);
      if (data) {
        if (data.message) {
          setMsg(data.message);
          return
        } else {
          setLoading(false);
        }
      }

      setCurrentWord({
        word: data[0].word,
        phonetic: data[0].phonetics[0].audio,
        meanings: data[0].meanings
      });
    }
    getWordData();
  }

  return (
    <ThemeContext.Provider value={{theme, setTheme}}>
      <div className={!theme ? 'app container-fluid light' : 'app container-fluid dark'}>
        <Search handleSubmit={handleSearchSubmit} />
        {loading && <div className="loading p-2">{msg}</div>}
        {!loading &&
          currentWord.word != '' ? <Word wordDescription={currentWord} /> : null
        }
        <Toggle />
      </div>
    </ThemeContext.Provider>
  )
}

export default App

We're creating a context using createContext() and then wrapping ThemeContext.Provider around our app. This makes sure that if ThemeContext is read in the other files, it can be used.

Also, you might notice we've added a ternary operator in className. We're checking whether theme is true or not. If it is, the theme is dark and vice-versa.

Now you just need to use this context in the rest of your files. Also, we haven't made the Toggle component yet, so I'll give the code and also explain how we're using our theme in it since the colour of the Toggle button itself changes too.

import './toggle.css';
import { ThemeContext } from './App';
import { useContext } from 'react';

const Toggle = () => {
    const {theme, setTheme} = useContext(ThemeContext);

    return (
        <div className='toggle p-2'>
            <button onClick={()=>setTheme(!theme)} className={!theme ? 'toggle-button rounded light' : 'toggle-button rounded dark'}>Toggle theme</button>
        </div>
    )
}

export default Toggle;

We're importing ThemeContext from App.jsx and using its value in the ternary operator for our button. If you go through the rest of the files in my repo, you'll notice that nowhere have we used the function returned by useContext, i.e, setTheme(). The only time when we want to change the theme is when the button gets clicked. When that does happen, we use setTheme(!theme) to alter its boolean value.

Conclusion

We're officially done building the app, at least in this blog post. Again, you need to check out my GitHub repo if you haven't understood where the code goes or how it works. Just visualize it on a piece of paper and you'll understand it better.

If you made it here, congrats! Thanks for reading the article and leave a comment if you have some feedback for me. Follow my profile on Hashnode if you want to read my blogs at the earliest!