If you haven’t already, you can find part one here. It describes how to get started, get Amplify initiated, and set up the API. In this post we will get our client side app up and running with React.
Building the UI
We already have our Create React App set up, so now we just have to write out some components and then wire up our Amplify code to get it all working.
First we need to let our React app know about Amplify. Luckily this is super easy. All we do is reference our aws-exports.js file that Amplify created for us, and pass that into the Amplify …..
Open up src/index.js and add the following code below the last import.
src/index.js
JSX
import Amplify from 'aws-amplify'
import config from './aws-exports'
Amplify.configure(config)
Boom! Now we are set up and ready to start using AWS.
There are a handful of components and styles that we need to build out, so let’s do that first.
In your src directory, add a new folder called components then add the following files:
Ask.js
BackHome.js
Hero.js
Question.js
QuestionItem.js
Again in your src directory add a folder called styles and then a file called main.scss
Copy the following and paste it into that file.
main.scss
SCSS
* {
box-sizing: border-box;
}
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
a {
text-decoration: none;
color: inherit;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: black;
font-size: inherit;
font-weight: inherit;
}
html,
body {
height: 100%;
}
body {
background-color: #faf089;
}
input {
border: 2px solid black;
width: 100%;
height: 3rem;
font-size: 1.2rem;
padding: 0rem 1rem;
flex: 1;
outline: none;
&.ask {
margin-bottom: 1rem;
&:focus {
box-shadow: 0 0 0 3px black;
}
}
}
.flex-center {
display: flex;
justify-content: center;
}
.container {
width: 100%;
margin-right: auto;
margin-left: auto;
padding-right: 1rem;
padding-left: 1rem;
display: flex;
flex-direction: column;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1440px) {
.container {
max-width: 1440px;
}
}
.hero {
text-align: center;
}
.navbar {
display: flex;
justify-content: flex-end;
padding: 1rem;
button {
margin-left: 1rem;
}
}
.hero {
margin-top: 5rem;
h1 {
margin-bottom: 2rem;
font-size: 2.5rem;
font-weight: bold;
}
}
.question-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 2rem 0rem;
border-bottom: 1px solid #cbd5e0;
flex-direction: column;
text-align: center;
&:first-child {
padding-top: 0px;
}
&:last-child {
padding-bottom: 0px;
border-bottom: 0px;
}
.question {
font-size: 2rem;
font-weight: bold;
margin-bottom: 1rem;
}
.answer {
margin-bottom: 2rem;
color: #4a5568;
}
}
@media (min-width: 640px) {
.question-item {
flex-direction: row;
text-align: left;
padding: 1rem 0rem;
.answer {
margin-bottom: 1rem;
}
}
}
button {
background-color: black;
padding: 0.5rem 0.7rem;
font-weight: 600;
font-size: 16px;
color: white;
outline: none;
border: none;
// margin-top: 5px;
// border-radius: 0.25rem;
cursor: pointer;
&:hover {
background-color: #363636;
}
&.teal {
background-color: #4fd1c5;
color: #2d3748;
&:hover {
background-color: #38b2ac;
}
}
}
.card {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
border-radius: 0.4rem;
padding: 1rem;
background-color: #fff;
flex: 1;
margin-top: 5rem;
margin-bottom: 3rem;
.heading {
font-size: 2rem;
font-weight: bold;
margin-bottom: 1rem;
}
.sub-heading {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 0.8rem;
}
.sub-text {
font-size: 1rem;
font-weight: bold;
margin-bottom: 0.3rem;
color: #4a5568;
}
.answer-section {
margin-top: 1.5rem;
border-top: 1px solid #cbd5e0;
padding-top: 1.5rem;
padding-bottom: 1rem;
}
}
App.js
JSX
import React, { useEffect, useState } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { API } from "aws-amplify";
// import all of our components
import Ask from "./components/Ask";
import Question from "./components/Question";
import QuestionItem from "./components/QuestionItem";
import Hero from "./components/Hero";
// import our query from the generated GraphQL
import { questionsByDate } from "./graphql/queries";
import "./styles/main.scss";
function App() {
const [allQuestions, setAllQuestions] = useState([]);
const [submittingQuestion, setSubmittingQuestion] = useState(false);
const [submittingAnswer, setSubmittingAnswer] = useState(false);
useEffect(() => {
fetchQuestions();
// refetch data when a question or answer have been submitted
}, [submittingAnswer, submittingQuestion]);
// fetch our questions from the API via Amplify
async function fetchQuestions() {
try {
const questionData = await API.graphql({
query: questionsByDate,
variables: {
type: "QUESTION",
sortDirection: "DESC",
},
});
const questionsArray = questionData.data.questionsByDate.items;
setAllQuestions(questionsArray);
} catch (err) {
console.log("FETCH QUESTIONS ERROR:: ", err);
}
}
return (
<Router>
<div className="container">
<Switch>
<Route path="/ask">
<Ask setSubmittingQuestion={setSubmittingQuestion} />
</Route>
<Route path="/question/:id">
<Question setSubmittingAnswer={setSubmittingAnswer} />
</Route>
<Route path="/">
<Hero />
<div className="card">
{allQuestions.map((question) => (
<QuestionItem questionPost={question} key={question.id} />
))}
</div>
</Route>
</Switch>
</div>
</Router>
);
}
export default App;
Let’s walk through the code a bit.
We will be handling some state, so we set those properties up using useState
We want to refetch our data when someone asks a or answers a question, so we can use useEffect
Our query actually takes place in the fetchQuestions function. We create an async function, then call the API passing it our questionsByDate query from the generated GraphQL Amplify made for us. If the call is successful, we set the data to state, if not we log what the error is.
In our return function, we have a very simple Router setup with a couple of routes and then a div that wraps our list of questions, each rendered in a <QuestionItem> component.
Ask.js
src/components/Ask.js
JSX
import React, { useState } from "react";
import { API } from "aws-amplify";
import { createQuestion } from "../graphql/mutations";
import BackHome from "./BackHome";
const Ask = ({ setSubmittingQuestion }) => {
const [question, setQuestion] = useState("");
const [saving, setSaving] = useState(false);
const [sent, setSent] = useState(false);
function onChangeText(e) {
e.persist();
setQuestion(e.target.value);
}
async function handleAsk(e) {
e.preventDefault();
try {
if (!question) return;
setSaving(true);
setSubmittingQuestion(true);
// Save the question to our DB
await API.graphql({
query: createQuestion,
variables: {
input: {
content: question,
type: "QUESTION",
},
},
});
setSaving(false);
setSent(true);
setSubmittingQuestion(false);
setQuestion("");
} catch (err) {
setSent(false);
setSaving(false);
setSubmittingQuestion(false);
console.log("ERROR SAVING QUESTION:: ", err);
}
}
return (
<>
<div className="card">
{saving && <p>sending question...</p>}
{!sent ? (
<form onSubmit={handleAsk}>
<h1 className="heading">ask me anything</h1>
<div>
<input
placeholder="be nice"
name="question"
onChange={onChangeText}
className="ask"
/>
</div>
<div className="flex-center">
<button type="submit">ask</button>
</div>
</form>
) : (
<p className="heading">question sent</p>
)}
</div>
<BackHome />
</>
);
};
export default Ask;
Again, nothing too crazy going on, but in our handleAsk function, we can see where we are calling the API in order to save the question to the database. We import the createQuestion mutation from the graphql folder and pass that into the API call along with the question text from the input. We have a couple uses of useState in order to handle the loading, and sent states in the UI.
BackHome.js
src/components/BackHome.js
JSX
import React from "react";
import { Link } from "react-router-dom";
const BackHome = () => {
return (
<Link to="/" className="flex-center">
<button>back home</button>
</Link>
);
};
export default BackHome;
We have a back home button on various screens, so we just abstracted it into its own component. Nothing fancy here.
Hero.js
src/components/Hero.js
JSX
import React from "react";
import { Link } from "react-router-dom";
const Hero = () => {
return (
<div className="hero">
<Link to="/">
<h1>Ron Swanson</h1>
</Link>
<Link to="/ask">
<button>ask me anything</button>
</Link>
</div>
);
};
export default Hero;
Same as our BackHome component, this is just used in a couple places, we it makes sense to break it into it’s own component.
Question.js
src/components/Question.js
JSX
import React, { useEffect, useState } from "react";
import { useParams } from "react-router";
import { API } from "aws-amplify";
import { getQuestion } from "../graphql/queries";
import { createAnswer } from "../graphql/mutations";
import BackHome from "./BackHome";
const Question = ({ setSubmittingAnswer }) => {
const { id } = useParams();
const [loading, setLoading] = useState(true);
const [question, setQuestion] = useState({});
const [answerInput, setAnswerInput] = useState("");
useEffect(() => {
fetchQuestion();
// eslint-disable-next-line
}, [id]);
async function fetchQuestion() {
try {
const question = await API.graphql({
query: getQuestion,
variables: {
id,
},
});
setQuestion(question.data.getQuestion);
setLoading(false);
} catch (err) {
setLoading(false);
console.log("FETCH QUESTION ERROR ", err);
}
}
function onChangeAnswer(e) {
e.persist();
setAnswerInput(e.target.value);
}
async function handleAddAnswer(e) {
e.preventDefault();
const answerInfo = {
questionID: question.id,
content: answerInput,
};
setSubmittingAnswer(true);
try {
await API.graphql({
query: createAnswer,
variables: {
input: answerInfo,
},
});
setSubmittingAnswer(false);
setAnswerInput("");
fetchQuestion();
} catch (err) {
setSubmittingAnswer(false);
console.log("ERROR ", err);
}
}
return (
<>
<div className="card">
{!loading && (
<>
<h1 className="heading">{question.content}</h1>
<p className="sub-text">
{(question.answer && question.answer.content) ||
"This question has not been answered yet"}
</p>
{!question.answer && (
<div className="answer-section">
<h3 className="sub-heading">add answer</h3>
<form
style={{ display: "flex", alignItems: "stretch" }}
onSubmit={handleAddAnswer}
>
<input
placeholder="super helpful answer"
onChange={onChangeAnswer}
value={answerInput}
style={{ flex: 1 }}
/>
<button type="submit">add answer</button>
</form>
</div>
)}
</>
)}
</div>
<BackHome />
</>
);
};
export default Question;
When a user clicks on a question from the home page, they will be taken to a page that will show the full question and answer, and allows us to write an answer.
We use React Router’s useParams hook to pull out the id from the url. We then use the id to fetch the question using the getQuestion query.
When we want to write an answer, we used the API from Amplify with the createAnswer mutation and pass it the current question.id and the answer text.
Lastly, if a question already has an answer, we don’t show the answer form. {!question.answer && (...)}
QuestionItem.js
src/components/QuestionItem.js
JSX
import React from "react";
import { Link } from "react-router-dom";
const QuestionItem = ({ questionPost }) => {
const { content, answer, id } = questionPost;
return (
<Link to={`/question/${id}`} className="question-item">
<div>
<h2 className="question">{content}</h2>
<p className="answer">{(answer && answer.content) || ""}</p>
</div>
<button>read more</button>
</Link>
);
};
export default QuestionItem;
Finally we have our QuestionItem component. This is rendered on the home page for each question that comes back to us. Pretty simple component. If there is an answer, we show that, and we also pass the id to the <Link> component so our Question component can use that to fetch the full question and answer from the database.
You can now go to your terminal and run either yarn start or npm start . You’ll see a pretty empty app so far, but you should now be able to ask a question, fetch the questions and answer a question.
Source: Paper.li
Comments