Building a Lyrics Finder App with the React Context API and TypeScript

Pieces 🌟 - Nov 21 '22 - - Dev Community

In this article, we’ll follow a React TypeScript tutorial on building a lyrics finder app. We’ll also discuss how to work with new and trending technologies like React Beautiful DnD and the React Context API. We’ll walk through using the Axios library to fetch data from an API and using Bootstrap CSS to manage the style.

After multiple code updates and enhancements, including type inferences, powerful static type checking, and understandability, TypeScript has grown in popularity. In this guide, we’ll learn how to use TypeScript with the React Context API by building a React lyrics finder app from scratch.

Prerequisites

Before delving further, you should note that we will build this app with TypeScript and React.js. You don't need to know how to write advanced TypeScript; I'll guide you through each step to get you going.

To get the most out of this tutorial, you need to have a basic understanding of the following:

  • Basic JavaScript
  • ES6 JavaScript
  • Basic TypeScript
  • Basic React

Setup and Installation

Let's set up and install a React app with TypeScript. Run this command to create the project "Lyrics App":

npx create-react-app Lyric-App --template typescript
Enter fullscreen mode Exit fullscreen mode

To install TypeScript, enter the following command:

npm install --save @types/react.
Enter fullscreen mode Exit fullscreen mode

To easily create a TypeScript project with CRA, you need to add the flag --template typescript, otherwise the app will only support JavaScript.

An easy-to-use React HTTP library called Axios makes it possible to manage and fetch data from APIs without any hassle. To install it, run:

npm install axios
Enter fullscreen mode Exit fullscreen mode

React DnD: With the help of this simple-to-use React library, lists can easily be moved using React. This is a tool that helps develop drag-and-drop functionalities very quickly and simply.

In the root folder, run the command:

npm install --save @types/react-beautiful-dnd
Enter fullscreen mode Exit fullscreen mode

React-Dom: For routing and managing the React DOM state, let's install react-router-dom with the command:

npm install react-router-dom
Enter fullscreen mode Exit fullscreen mode

Let's add Bootstrap CSS after that. The best way to use React-Bootstrap is through the npm package, which you can install with npm (there’s also a yarn package if you prefer).

npm install react-bootstrap bootstrap
Enter fullscreen mode Exit fullscreen mode

After installation is complete, your package.json should look like this:

{

"name": "typ",

"version": "0.1.0",

"private": true,

"dependencies": {

"@testing-library/jest-dom": "^5.16.5",

"@testing-library/react": "^13.4.0",

"@testing-library/user-event": "^13.5.0",

"@types/jest": "^27.5.2",

"@types/node": "^16.11.59",

"@types/react": "^18.0.20",

"@types/react-beautiful-dnd": "^13.1.2",

"@types/react-dom": "^18.0.6",

"axios": "^0.27.2",

"react": "^18.2.0",

"react-beautiful-dnd": "^13.1.1",

"react-dom": "^18.2.0",

"react-router-dom": "^6.4.1",

"react-scripts": "5.0.1",

"typescript": "^4.8.3",

"web-vitals": "^2.1.4"

},
Enter fullscreen mode Exit fullscreen mode

We’re done with our setup! Let’s start writing some code.

Create Your API Token

Before we get into building, we need an API token to run and fetch music lyrics. For this tutorial, we will use the Musixmatch API token. Create a new account on Musixmatch for a unique token.

Open an account and navigate to the Dashboard.

Click the Applications button and scroll down.

The Musixmatch dashboard.

Your Applications dashboard contains your API token and your username.

The list of your Musixmatch applications.

Copy the API token and include it in a .env file in the root folder as so:

REACT_APP_MM_KEY= "Input Token here"
Enter fullscreen mode Exit fullscreen mode

Fetching Lyrics Data from the React Context API

In React v16, the React Context API was added as a mechanism to communicate data among components without passing props down at each level.

It's good practice to have distinct type definition files because it strengthens the project's structure. The stated types can either be utilized explicitly by importing them into another file or by reference without importing them (though they have to be exported first).

Now that this is established, we can get our hands dirty and write some useful code.

//Context.tsx
import React, { useState, useEffect, createContext } from "react";
import axios from "axios";

interface ContextPro {
 track_list?:({} | null)[] | string[] | number ;
 heading?: ({} | null)[] | [] |" ";

}
export const Context = createContext({} as ContextPro );
export const ContextP: React.FC<React.PropsWithChildren> = ({ children }) =>  {
 const [state, setState] = useState<ContextPro[] | null | {} | string[]>([
{
 track_list:[],
 heading:" ",
 },

]);

useEffect(() => {
 axios
  .get(

`https://cors-anywhere.herokuapp.com/http://api.musixmatch.com/ws/1.1/chart.tracks.get?page=1&page_size=10&country=us&f_has_lyrics=1&apikey=${
process.env.REACT_APP_MM_KEY
  }`
 )
 .then(res => {
  console.log(res.data);
 setState({
  track_list: res.data.message.body.track_list,
  heading: "Top 10 Tracks"
 });
 })
  .catch(err => console.log(err));
 }, []);
}
Enter fullscreen mode Exit fullscreen mode

As you can see from the code written above, the ContextPro interface defines the types which expect an array or null object value or string type for track_list and heading.

While fetching React Context API data, observe the URL link before the main API data “https://cors-anywhere.herokuapp.com”. This link enables us to access the API data. The API data returns an error due to CORs restrictions. Hence, the CORs link above accounts for the error and grants access to the API.

When creating the context, we set the default state value to null or an empty array temporarily; the intended values will be assigned by the provider. Here, I initialized the state with some data to have lyrics.tsx work.

Only the components that require the data will receive it thanks to the context. Next, we import the context into App.js and wrap the context around the parent-level component. Here is how the App component looks:

//App.tsx
import React from 'react';
import {
 BrowserRouter as Router,
 Routes,
 Route,
} from "react-router-dom";
import Navbar from './Components/Navbar';
import Home from './Components/Home'
import Lyric from './Components/Lyric';
import { ContextP } from './Components/Context';

function App() {
 return (
  <ContextP>
  <Router>
    <Navbar />
     <div className="container">
       <Routes>
        <Route path='/' element={<Home />} />
        <Route path="/lyric/track/:id" element={<Lyric />} />
       </Routes>
     </div>

</Router>
</ContextP>

);}
export default App;
Enter fullscreen mode Exit fullscreen mode

Refactor the context to provide data and pass it to various child components as so:

// Context.tsx

return (
 <Context.Provider value={[state, setState]}>
   {children}
   </Context.Provider>
);
Enter fullscreen mode Exit fullscreen mode

The values are then passed to the context so that the components can consume them as above.

Create the Components and Consume the Context API

We’ll build a <Search> component that lets a user input a song title and <LyricLists> and <Lyrics> components to display the lyrics search results in a mapped and ordered pattern.

Finally, a <Lyric> component displays the actual lyrics when clicked.

Let's begin by making a new folder in the src directory named “Components” because that's where all of our components will be. Let's now develop the components for <Lyrics>, <LyricLists> and <Search>. They must then be imported into our App.js code.

Using the useState hook, the <Search> component below lets us manage user-entered data. Once we get the form data, we utilize the context object's setState function to show it on the lyricLists component.

First, we’ll create a function that makes an API call to Musixmatch using the Axios library:

//Search.tsx
import React, { useState, useEffect, useContext } from "react";
import axios from "axios";
import { Context } from "./Context";

const Search = () => {
  const ctxt = useContext(Context);
  const [state, setState]: {} | any = ctxt;
  const [userInput, setUserInput] = useState("");
  const [trackTitle, setTrackTitle] = useState("");
  useEffect(() => {
    axios
      .get(
        `https://cors-anywhere.herokuapp.com/http://api.musixmatch.com/ws/1.1/track.search?q_track=${trackTitle}&page_size=10&page=1&s_track_rating=desc&apikey=${process.env.REACT_APP_MM_KEY}`
      )
      .then((res) => {
        let track_list = res.data.message.body.track_list;
        setState({ track_list: track_list, heading: "Search Results" });
      })
      .catch((err) => console.log(err));
  }, [trackTitle]);
  const findTrack = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setTrackTitle(userInput);
  };
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setUserInput(e.target.value);
  };
};
export default Search;
Enter fullscreen mode Exit fullscreen mode

Note that I use typecasting on the useContext hook to prevent TypeScript from throwing errors because the context will be null or an empty array at the beginning.

Then, to confirm that we are receiving results from the API, we perform an API request on Axios using the GET method. We then use the then-catch block to obtain the API response that follows. In addition, we take advantage of the React Typescript useState hook.

We now need to show the data to the user in our app after successfully obtaining it from the API. We'll create a very basic search field where a user may enter the title of their favorite song lyrics.

We'll ask the Musixmatch API for the lyrics information and show the result in our user interface.

return (
 <div className="card card-body mb-4 p-4">
  <h1 className="display-4 text-center">
   <i className="fas fa-music" /> Search For Lyrics
  </h1>
  <p className="lead text-center">Get the lyrics for any song</p>
  <form onSubmit={(e) =>findTrack(e)}>
   <div className="form-group">
    <input
     type="text"
     className="form-control form-control-lg"
     placeholder="Song title..." 
     name="userInput"
     value={userInput}
     onChange={onChange}
    />
   </div>
   <button className="btn btn-primary btn-lg btn-block mb-7" type="submit">
    Get Track Lyrics
   </button>
  </form>
 </div>
);
Enter fullscreen mode Exit fullscreen mode

In the return section, we have a search input that accepts a name and listens for an event to perform a search or call the API.

//Lyrics.tsx
import React, {useContext} from 'react'
import LyricLists from './LyricLists';
import { Context} from './Context'
import{ Droppable, DragDropContext  } from 'react-beautiful-dnd'
const Lyrics = () =>{
 const ctxt = useContext(Context)  ;
 if (ctxt == null) return <div>No context yet</div>;
 const [state]:any = ctxt
 const { track_list, heading } = state;

if (track_list === undefined || track_list.length === 0) {
 return <h1>Loading...</h1>;
} else {

 const onDragEnd =(result:any) => {
  if(!result.destination)
  return;
}
return (
 <>
  <DragDropContext onDragEnd={onDragEnd} >
  <Droppable droppableId="droppable">
  {(provided) => (

   <div{...provided.droppableProps}
   ref={provided.innerRef}>
   <h3 className="text-center mb-4">{heading}</h3>
  <div className="row">
  {track_list.map((item:any, index:number) => (
   <LyricLists
   key={item.track.track_id} track={item.track} index={index} />
  ))}{provided.placeholder}
 </div>
</div>
 )}
 </Droppable>
 </DragDropContext>
</>
);}};
export default Lyrics;
Enter fullscreen mode Exit fullscreen mode

As you can see above, we have a presentational component that shows a map listing of lyrics. It receives the state value from the context alongside track_list and heading from a destructured state object and the function to update it as parameters that need to match the Props type defined in the context.

We also imported some methods from React Beautiful DnD to handle the droppable content area.

DragDropContext is going to give our app the ability to use the library. It works similarly to the React Context API; notice how the entire <lyriclists> component is wrapped around the dragdropcontext.

With the aid of the ref, Droppable gives you the ability to drop an item into a list where its properties are inherited.

//LyricLists
import React from 'react';
import { Link } from 'react-router-dom';
import { Draggable} from 'react-beautiful-dnd'
interface props {
 track: any;index:number}
const LyricLists: React.FC<props> = ({
 track,index,
}) => {
return (
  <Draggable draggableId={track.track_id} index={index} >
  {(provided) => (
  <div className="col-md-6"ref= {provided.innerRef}
   {...provided.draggableProps}
   {...provided.dragHandleProps} draggable >
  <div className="card mb-4 shadow-sm">
   <div className="card-body" draggable >
    <h5>{track.artist_name}</h5>
    <p className="card-text">
     <strong>
      <i className="fas fa-play" /> Track
     </strong>
     : {track.track_name}
     <br />
     <strong>
      <i className="fas fa-compact-disc" /> Album
     </strong>
     : {track.album_name}</p>
    <Link
     to={`/lyric/track/${track.track_id}`}
     className="btn btn-dark btn-block">
     <i className="fas fa-chevron-right" /> View Lyrics
    </Link>
   </div>
  </div>
 </div>
  )}
 </Draggable>
);};
export default LyricLists;
Enter fullscreen mode Exit fullscreen mode

After destructuring the track from <Lyrics> components, import and wrap <LyricLists> with a draggable method. By clicking and dragging the draggable object with the mouse, you can move it around the viewport.

Next, we create a <Lyric> component to display individual lyric data as well as artist name and track_id. This component contains a unique link id which was created using the useParams() hook and can only be accessed from inside <LyricLists> components.

Notice how we applied the useEffect hook, which allows us to interact with the environment without affecting the rendering of the component.

useParams returns an object of key/value pairs of URL parameters. This gives a unique key to the route access. Hence, using params.id as a dependency for the useEffect hook enables the Axios API call to only run when we click on “view lyrics.”

//Lyric.tsx
import React, { useState, useEffect } from "react";
import axios from "axios";
import { Link, useParams } from "react-router-dom";
//import Moment from "react-moment";

const Lyric = () => {
 const [track, setTrack] = useState<any>([]);
 const [lyrics, setLyrics] =useState<any>([]);
 const params = useParams();

useEffect(() => {
 axios
  .get(

`https://cors-anywhere.herokuapp.com/http://api.musixmatch.com/ws/1.1/track.lyrics.get?track_id=${
    params.id
   }&apikey=${process.env.REACT_APP_MM_KEY}`
  )
  .then(res => {
   let lyrics = res.data.message.body.lyrics;
   setLyrics({ lyrics });

   return axios.get(
`https://cors-anywhere.herokuapp.com/http://api.musixmatch.com/ws/1.1/track.get?track_id=${
    params.id
   }&apikey=${process.env.REACT_APP_MM_KEY}`
  );
 })
 .then(res => {
  let track = res.data.message.body.track;
  setTrack({ track });
 })
  .catch(err => console.log(err));
}, [params.id]);

if (
 track === undefined ||
 lyrics === undefined ||
 Object.keys(track).length === 0 ||
 Object.keys(lyrics).length === 0
) {
 return <h1>Loading...</h1>;
} else {
}
};

export default Lyric;
Enter fullscreen mode Exit fullscreen mode

Then we have the return div, which displays details of the music data like name, year, artist, release date, etc. You can display as many details as you want.

return ( <>
   <Link to="/" className="btn btn-dark btn-sm mb-4">
    Go Back
   </Link>
   <div className="card">
    <h5 className="card-header">
    {track.track.track_name} by{" "}
    <span className="text-secondary">{track.track.artist_name}</span>
   </h5>
   <div className="card-body">
    <p className="card-text">{lyrics.lyrics.lyrics_body}</p>
   </div>
  </div>

  <ul className="list-group mt-3">
   <li className="list-group-item">
    <strong>Album ID</strong>: {track.track.album_id}
   </li>
   <li className="list-group-item">
    <strong>Song Genre</strong>:{" "}
    {track.track.primary_genres.music_genre_list.length === 0
     ? "NO GENRE AVAILABLE"
     : track.track.primary_genres.music_genre_list[0].music_genre
       .music_genre_name}
   </li>
   <li className="list-group-item">
    <strong>Explicit Words</strong>:{" "}
    {track.track.explicit === 0 ? "No" : "Yes"}
   </li>
   <li className="list-group-item">
    <strong>Release Date</strong>:{" "}
   </li>
  </ul>
 </>
);
Enter fullscreen mode Exit fullscreen mode

Conclusion

Awesome! Our app now does all tasks. Here is a summary of what we did:

We received an API key for the Musixmatch API. We also created a component that enables title-based lyrics searches and stores the results in the component's state.

The function was then sent to the search form so that it would take effect when we clicked the button or pressed enter. After that, we created a lyric component that shows the response information we had obtained from the React Context API and put the response in a single lyric state that can be accessed using the useparam hook.

The repository for the component library developed in this article can be found on my GitHub.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .