I want to build a flashcard app.
I'm imagining a dataset of questions/answers and I can display each question as a "card". When I click on the card, it will "flip" and show me the answer.
I don't have as clear of a picture of how I will implement these details so when planning, I'll list these features as user stories:
- Users can keep score
- There is a form so that users can add new flashcards
- Users can make "playlists" or "quizzes" that show a specific set of flashcards
First, create the app using Vite: npm create vite@latest
(you may be prompted to install the latest version of vite)
npm create vite@latest
# Name it flashcards
# Select React
# Select JavaScript
cd flashcards
npm i
# Delete the contents of App.jsx
I know that I'll need some flashcard data to render.
To get myself started, I used ChatGPT to give me a dataset of flashcards rather than come up with my own set of questions. I asked for the data in JSON format so that I could easily import it into my app.
I then stored the resulting data in a .json
file called src/db/flashcards.json
(I made a src/db
folder since this is sort of like my "database").
{
"flashcards": [
{
"id": 1,
"question": "What is React?",
"answer": "React is a JavaScript library for building user interfaces."
},
{
"id": 2,
"question": "What is JSX?",
"answer": "JSX is a syntax extension for JavaScript used with React to describe what the UI should look like."
},
{
"id": 3,
"question": "What are components in React?",
"answer": "Components are the building blocks of a React application. They encapsulate logic and UI."
},
...
]
}
ChatGPT did a great job of giving me data in a format that I could easily use.
- The data was in an Array which means I can render a "card" for each object using
.map()
in aul
- Each flashcard object had:
- an
id
which I can use for list itemkey
props and much more - a
question
and ananswer
which will be useful for when I want to toggle which text I show to the user.
- an
JSON Server is a tool to we use to spin up a mock API. It basically lets us turn any properly formatted .json
file into a full API running on localhost
.
It is a great alternative when you don't have the time to build out a full Express API. It does have its limitation in that it cannot support a robust relationships database. Read the JSON Server documentation for more information.
Using the JSON file we created above, we can create a mock API. To set it up we can:
- Run
npm install -g json-server
to install json server globally - Create the
.json
file. We did this already:db/flashcards.json
- From the root of your vite project, split your terminal and run
json-server --watch db/flashcards.json --port 4000
to start a mock back-end server on port 4000.
- Now, you will have an API that you can access via the URL http://localhost:4000/flashcards (try visiting that URL in your browser!)
json-server
only works if the .json
file is in the proper format. The JSON file needs to store a JSON object with a top-level property that names the resource to be fetched.
Something like (feel free to copy this):
{
"flashcards": [
{
"id": 1,
"question": "What is React?",
"answer": "React is a JavaScript library for building user interfaces."
},
{
"id": 2,
"question": "What is JSX?",
"answer": "JSX is a syntax extension for JavaScript used with React to describe what the UI should look like."
},
{
"id": 3,
"question": "What are components in React?",
"answer": "Components are the building blocks of a React application. They encapsulate logic and UI."
}
]
}
In this example, "flashcards"
is the top-level property which makes http://localhost:4000/flashcards a valid endpoint. When we send a GET
request to that endpoint, we'll get back the value of "flashcards"
.
Q: What would be the endpoint(s) created if this were our JSON file?
{
"friends": [
"ben",
"gonzalo",
"carmen"
],
"message": {
"data": "hello world"
}
}
Q: How would I make a http://localhost:4000/flashcards/react or http://localhost:4000/flashcards/fetch endpoint?
Answer
{
"flashcards": {
"react": [
{
"id": 1,
"question": "What is React?",
"answer": "React is a JavaScript library for building user interfaces."
},
...
],
"fetch": [
{
"id": 1,
"question": "What does fetch do?",
"answer": "fetch sends an HTTP request to the provided url."
},
...
]
}
}
To make the MVP, the app can be quite simple. Just render a ul
with an li
"card" for each flashcard object. So I basically just need my App
component and Flashcard
component. I'll then map each object in the dataset to a <Flashcard />
.
For the MVP, here is what I came up with:
The Flashcard
component should be focused solely on rendering a single flashcard
object. It can maintain its own state to toggle back and forth between showing the question and the answer.
import { useState } from 'react'
const Flashcard = ({ flashcard }) => {
const [text, setText] = useState(flashcard.question)
const [backgroundColor, setBackgroundColor] = useState('lightblue')
const flipCard = () => {
if (text === flashcard.question) { // show the answer
setText(flashcard.answer);
setBackgroundColor('lightgreen');
} else {
setText(flashcard.question); // show the question
setBackgroundColor('lightblue');
}
}
// set the style dynamically using the backgroundColor state
const style = { background: backgroundColor }
return (
<li className="card" onClick={flipCard} style={style}>
<p>{text}</p>
</li>
)
}
- The
Flashcard
component takes in aflashcard
object as a prop. - It also keeps track of two state values:
text
andbackgroundColor
which can be toggled between showing the question and showing the answer - We provide a
style
prop to dynamically set the style of the component using thebackgroundColor
state - We render the flashcard as an
li
with anonClick
prop, astyle
prop, and with thetext
state rendered.
The App component needs to fetch the set of flashcards from the json-server URL http://localhost:4000/flashcards when the component first loads and then use that data to render a list of flashcards.
import './App.css'
import { useState, useEffect } from 'react'
import fetchData from './utils/fetchData'
// Check out the helper function ^
const Flashcard = () => {
// flashcard component
}
function App() {
const [flashcards, setFlashcards] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
const doFetch = async () => {
const [data, error] = await fetchData('http://localhost:4000/flashcards');
if (data) setFlashcards(data);
if (error) setError(error);
};
doFetch();
}, []); // run the effect only once
// Conditionally render the error message
if (error) return <p>{error.message}. Refresh to try again.</p>
return (
<>
<h1>Flash Cards</h1>
<ul>
{
flashcards.map((flashcard) => <Flashcard key={flashcard.id} flashcard={flashcard} />)
}
</ul>
</>
)
}
export default App
Let's break it down:
- The
App
keeps track offlashcards
anderror
state. - We use
useEffect
to fetch the flashcard data from our json-server when the component first renders (and only that one time).- Then we either invoke
setFlashcards
orsetError
depending on the returned data.
- Then we either invoke
- The
App
component maps over theflashcards
data, creating aFlashcard
component for eachflashcard
object. - When rendering a list of components, we use the
flashcard
object'sid
as thekey
and pass along theflashcard
object as a prop. - Since I kept the starting CSS styles that came with the Vite project, it actually looks okay.
- Organize my components into separate files
- Build out stretch features
- Add better styling