Building a Lyrics Finder App with the React Context API and TypeScript
Pieces ๐
Posted on November 21, 2022
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
To install TypeScript, enter the following command:
npm install --save @types/react.
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
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
React-Dom: For routing and managing the React DOM state, let's install react-router-dom
with the command:
npm install react-router-dom
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
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"
},
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.
Your Applications dashboard contains your API token and your username.
Copy the API token and include it in a .env
file in the root folder as so:
REACT_APP_MM_KEY= "Input Token here"
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));
}, []);
}
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;
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>
);
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;
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>
);
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;
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;
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;
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>
</>
);
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.
Posted on November 21, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024
November 23, 2024
November 16, 2024