In this article, we will build a startup site with forms functionality. The startup will be an estate site that helps us find new homes. We will use Strapi for the backend and React for the frontend.
Also, we will have a newsletter form for users who want to receive monthly emails about our products. On signup, the user will also receive a confirmation email that the signup was successful.
Strapi is an open-source headless CMS solution that is self-hosted to allow customization and flexibility for your backend API service. One advantage of Strapi is the ease of scaffolding an API to expose endpoints that can be used as micro-services in your frontend. Strapi supports both a RESTful and GraphQL API.
Before we continue this article, it is pertinent you know;
Strapi’s CLI installation guide requires two things to be installed in your computer:
Npm: Only v6 of npm is suported by Strapi. To downgrade to v6, run npm install npm@6
.
Postman: We will be using postman to test our Strapi endpoints before connecting it to our React Frontend.
We will be using Strapi for our backend setup; hence, we will install and scaffold a Strapi project.
Note: This article is written with the latest version of Strapi(v4.1.2) and tested with Strapi 4.1.2(Node 16 and yarn).
To install and scaffold a Strapi project, create a file called Marketing-Site, cd into it, and run either of the following commands below:
1 cd Marketing-Site
2 npx create-strapi-app@latest backend --quickstart
or
1 cd Marketing-Site
2 yarn create strapi-app backend --quickstart
From above, we are creating a project called backend
inside our Marketing-Site folder and using the --quickstart flag to choose the quickstart installation type.
After installation, run yarn develop
to start the new project. The command will open up a tab in your browser with a page to register your new admin. Fill out the form and submit it to log in to your Strapi dashboard.
After registering, we will be directed to the admin homepage to set up our backend content and APIs.
Let us create an Estate collection type. On your admin homepage, click on Content-Type Builder and then Create a new collection type.*
A modal will prompt you to create your content type and enter “Estate” as your Content-Type name because that is what we are building. Click on Continue and add the different fields you want on your website.
For this tutorial, we only need the Name, Description, Image and Price of our houses.
After you have selected the fields, click on Finish to leave the modal. There are Save and Publish buttons at the top-right side of our page. Click on Save to save the collection and Publish to publish our collection. You should have something like the image below;
Click on Content Manager at the top-left side of the admin page. This will navigate you to the page where we will populate our database.
Click on Add new entry to add different houses. We will be needing the name, description, pictures and price of the house so go ahead and populate the collection with your data. After you have done that, click on the publish button on the top-right side of your page to publish the collection.
Now, let’s make the estate route available to consume it in our React frontend. Click on Settings to go to the settings page and click on Roles under Users and Permissions Plugins.
Next, click on Public and then check the find
and findOne
options under the Estate dropdown. This will make these two estate routes publicly available.
You can test the routes with Postman to see how and what our Strapi API returns. Below is what the http://localhost:1337/api/estates
will return:
Let’s create our frontend with React, install axios and react router packages, and connect them to Strapi. To spin up our React app, we will first exit or cd out of the backend folder before creating our React app, or we can open up a new terminal and run the commands below inside the Marketing-Site folder:
1 npx create-react-app frontend
2 cd frontend
3 npm i axios react-router-dom --save
4 npm start
Below is what our folder structure will look like. I will also explicitly state and explain the pages so you won’t get lost.
I’ve written the code below according to the folder structure. You can copy and paste accordingly. You can also clone the Github repo here to access the CSS styling.
1// frontend/src/hooks/useFetch.js
1 import { useState, useEffect } from 'react';
2 import axios from 'axios';
3 export default function useFetch(url) {
4 const [ estate, setEstate ] = useState(null);
5 const [ error, setError ] = useState(null);
6 const [ loading, setLoading ] = useState(true);
7
8 useEffect(
9 () => {
10 const fetchData = async () => {
11 setLoading(true);
12 try {
13 const res = await axios.get(url);
14 setEstate(res.data.data);
15 setLoading(false);
16 } catch (error) {
17 setError(error);
18 setLoading(false);
19 }
20 };
21 fetchData();
22 },
23 [ url ]
24 );
25
26 return { estate, error, loading };
27 }
1// frontend/src/pages/about/About.js
1 import React from 'react';
2 import { useParams, Link } from 'react-router-dom';
3 import classes from './about.module.css';
4 import useFetch from '../../hooks/useFetch';
5
6 function AboutPage() {
7 const { id } = useParams();
8 const { loading, error, estate } = useFetch(`http://localhost:1337/api/estates/${id}?populate=*`);
9 if (loading) return <p> Loading... </p>;
10 if (error) return <p> Error :( </p>;
11
12 return (
13 <article className={classes.aboutPage}>
14 <h2>More Description</h2>
15 <hr />
16 <section className={classes.aboutBoard}>
17 <h2>{estate.attributes.name}</h2>
18 <div className={classes.aboutDescription}>
19 <div className={classes.aboutImgContainer}>
20 {estate.attributes.image.data ? (
21 estate.attributes.image.data.map((pic) => (
22 <img
23 src={`http://localhost:1337${pic.attributes.url}`}
24 alt="img"
25 key={pic.attributes.id}
26 />
27 ))
28 ) : (
29 <img
30 src={`http://localhost:1337${estate.attributes.image.data.attributes.url}`}
31 alt="img"
32 />
33 )}
34 </div>
35 <div>
36 <h3>{estate.attributes.price}</h3>
37 <p>{estate.attributes.description}</p>
38 <Link
39 to={'/'}
40 style={{
41 textDecoration: 'none',
42 background: 'black',
43 color: 'white',
44 border: '1px solid black',
45 padding: '5px 10px'
46 }}
47 >
48 {'< Back to Home'}
49 </Link>
50 </div>
51 </div>
52 </section>
53 </article>
54 );
55 }
56 export default AboutPage;
1// frontend/src/pages/estates/Estates.js
1 import React from 'react';
2 import { Link } from 'react-router-dom';
3 import useFetch from '../../hooks/useFetch';
4 import classes from './estates.module.css';
5
6 export default function Estatepage() {
7 const { estate, error, loading } = useFetch('http://localhost:1337/api/estates?populate=*');
8 if (loading) return <p> Loading... </p>;
9 if (error) return <p> Error :( </p>;
10
11 return (
12 <div className={classes['estates']}>
13 <section>
14 <h2>Available Houses</h2>
15 <hr className={classes['horizontal-rule']} />
16 {estate.map((house) => (
17 <article className={classes['article']} key={house.id}>
18 <h2>{house.attributes.name}</h2>
19 <section className={classes['article-description']}>
20 <img
21 src={`http://localhost:1337${house.attributes.image.data[0].attributes.url}`}
22 alt="img"
23 />
24 <div>
25 <p>{house.attributes.price}</p>
26 <p>{house.attributes.description}</p>
27 <Link to={`${house.id}`}>See More...</Link>
28 </div>
29 </section>
30 </article>
31 ))}
32 </section>
33 </div>
34 );
35 }
1// frontend/src/pages/home/Home.js
1 import React from 'react';
2 import { Link } from 'react-router-dom';
3 import useFetch from '../../hooks/useFetch';
4 import classes from './home.module.css';
5
6 export default function Homepage() {
7 const { estate, error, loading } = useFetch('http://localhost:1337/api/estates?populate=*');
8 if (loading) return <p> Loading... </p>;
9 if (error) return <p> Error :( </p>;
10
11 return (
12 <div className={classes['home']}>
13 <section>
14 <h2>Welcome to our Estate</h2>
15 <hr className={classes['horizontal-rule']} />
16 <p>We help you find your new home</p>
17
18 <form className={classes["home-form"]}>
19 <h5>Interested in joining our Newsletter</h5>
20 <h6>Sign up with your email below</h6>
21
22 <label htmlFor="email">
23 Email Address:
24 <input type="email" />
25 </label>
26 <button>Signup</button>
27 </form>
28 {estate.splice(0, 2).map((house) => (
29 <article className={classes['home-article']} key={house.id}>
30 <h2>{house.attributes.name}</h2>
31 <section className={classes['home-article-description']}>
32 <img
33 src={`http://localhost:1337${house.attributes.image.data[0].attributes.url}`}
34 alt="img"
35 />
36 <div>
37 <p>{house.attributes.price}</p>
38 <p>{house.attributes.description}</p>
39 <Link to={`estates/${house.id}`}>See More...</Link>
40 </div>
41 </section>
42 </article>
43 ))}
44 </section>
45 </div>
46 );
47 }
1// frontend/src/pages/nav/Nav.js
1 import React from 'react';
2 import { Link } from 'react-router-dom';
3 import classes from './nav.module.css';
4
5 export default function NavHeader() {
6 return (
7 <div className={classes.navBar}>
8 <h1>My Estate</h1>
9 <nav className={classes.navLink}>
10 <ul>
11 <Link to="/" style={{ textDecoration: 'none' }}>
12 <li>Home</li>
13 </Link>
14 <Link to="estates" style={{ textDecoration: 'none' }}>
15 <li>Estates</li>
16 </Link>
17 </ul>
18 </nav>
19 </div>
20 );
21 }
1// frontend/src/App.js
1 import React, { Suspense } from 'react';
2 import { Routes, Route } from 'react-router-dom';
3 import Nav from './pages/nav/Nav';
4 import Home from './pages/home/Home';
5
6 const About = React.lazy(() => import('./pages/about/About'));
7 const Estates = React.lazy(() => import('./pages/estates/Estates'));
8
9 export default function App() {
10 return (
11 <div>
12 <Nav />
13 <Routes>
14 <Route path="/" element={<Home />} />
15 <Route
16 path="estates"
17 element={
18 <Suspense fallback={<p>Loading...</p>}>
19 <Estates />
20 </Suspense>
21 }
22 />
23 <Route
24 path="estates/:id"
25 element={
26 <Suspense fallback={<p>Loading...</p>}>
27 <About />
28 </Suspense>
29 }
30 />
31 </Routes>
32 </div>
33 );
34 }
Inside App.js file above, we implemented a React 18 feature called Suspense API.
According to React’s official page, “Suspense is a new feature that lets you also use <Suspense>
to declaratively “wait” for anything else, including data. It’s a mechanism for data-fetching libraries to communicate to React that the **data a component is reading is not ready yet. React can then wait for it to be ready and update the UI”.
1// frontend/src/index.js
1 import React from 'react';
2 import ReactDOM from 'react-dom';
3 import { BrowserRouter } from 'react-router-dom';
4 import App from './App';
5
6 ReactDOM.render(
7 <React.StrictMode>
8 <BrowserRouter>
9 <App />
10 </BrowserRouter>
11 </React.StrictMode>,
12 document.getElementById('root')
13 );
Here’s what the files above do:
useFetch.js
file. By doing this, we wouldn’t have to rewrite the same logic every time. Instead, we will invoke it in the components we need it. Nav.js
contains the static nav header on our website that houses the website’s name and the Estate and Home anchor links.Home.js
has a form element. We will be using this form for our newsletter signup, which is one of the main focus of this article.1 // Yarn
2 yarn start
1 //Npm
2 npm start
Now, we can easily change and add content with Strapi by simply editing any of the current Collection Types or creating new entries.
We have completed the frontend setup and integration. Next, we’ll work on our newsletter integration.
There are so many email providers out there like Mailchimp, MailerLite, Sendinblue, and so on. But for this article, we will be using an email provider called SendGrid.
To set up a SendGrid service, we will first create a SendGrid account. This is because we need to connect SendGrid to Strapi through the SendGrid API. Head over to SendGrid to sign up and create your account.
After logging into your dashboard, click on the S**ettings dropdown on the left side of the dashboard and click on Sender Authentication. Proceed to create a new sender and make sure to verify the email address too.
Next, we will create our API key. On the left side of the dashboard, click on the Settings dropdown again and click on API keys. Click on create API key, give it a name and copy your API key.
Note: Make sure to copy your API key and store it somewhere safe because SendGrid won’t show it to you again. You can always create a new key though if you loose or forget where you stored it.
Next, we will go into our backend folder and run any of the commands below to download strapi email provider plugin.
1 // using yarn
2 yarn add @strapi/provider-email-sendgrid --save
3
4 // using npm
5 npm install @strapi/provider-email-sendgrid --save
After we have successfully downloaded the plugin, we’ll set it up in our backend folder. In the config
folder, create a new file called plugins.js
and paste the code below:
1// config/plugins.js
1 module.exports = ({ env }) => ({
2 email: {
3 provider: 'sendgrid',
4 providerOptions: {
5 apiKey: env('SENDGRID_API_KEY')
6 },
7 settings: {
8 defaultFrom: 'myemail@protonmail.com',
9 defaultReplyTo: 'myemail@protonmail.com'
10 }
11 }
12 });
Replace the settings default emails with your SendGrid verified email. Also, in your .env
file, add your SendGrid API key.
1SENDGRID_API_KEY=SG.5hoLikrVQXudcUtgaV6n6g.aKttCp***********************************
After that, head over to the api
folder inside src
folder and create a new folder called subscribe
. Inside our subscribe
folder, we will also create two extra folders: config
and controllers
. In our config folder, create a new routes.json
file and add the code below.
1// src/api/subscribe/config/routes.json
1 {
2 "routes": [
3 {
4 "method": "POST",
5 "path": "/email",
6 "handler": "email.send",
7 "config": {
8 "policies": []
9 }
10 }
11 ]
12 }
Then, in our controllers folder create an email.js file, and add the following code
1// src/api/subscribe/controllers/email.js
1 module.exports = {
2 send: async (ctx) => {
3 let options = ctx.request.body;
4
5 await strapi.plugins.email.services.email.send({
6 to: options.to,
7 from: 'yourmail@gmail.com',
8 replyTo: 'yourmail@gmail.com',
9 subject: options.subject,
10 text: options.html
11 });
12
13 ctx.send('Email sent!');
14 }
15 };
We will now test our configuration in Postman and see what we get. Before that, make sure you make the email route publicly available in your Strapi admin settings.
(Settings > Users and Permissions Plugin > Roles > Public > Email)
Then, in our postman, let’s test our API to see if it works.
We can see we got a status of 200 meaning the request was sent successfully. Log in to your email account to see the test message.
Finally, We will now integrate our Strapi subscribe functionality into our React app.
Head over to your frontend
folder. Under the hooks folder where we created our useFetch.js
file, create a new file called usePost.js
. We will be putting our POST logic here; then, we will import it into our Home file.
1// frontend/src/hooks/usePost.js
1 import { useState } from 'react';
2 import axios from 'axios';
3 const usePost = (url) => {
4 const [ signup, setSignup ] = useState('');
5 const [ signupError, setError ] = useState(null);
6 const [ signupMsg, setSignupMsg ] = useState('');
7 const [ signupLoading, setSignupLoading ] = useState(true);
8
9 const handleChange = (e) => {
10 setSignup(e.target.value);
11 };
12
13 const handleSignup = (e) => {
14 e.preventDefault();
15 let userData = {
16 to: signup,
17 from: 'chimezieinnocent39@gmail.com',
18 replyTo: 'chimezieinnocent39@gmail.com',
19 subject: 'Thanks for signing up',
20 html:
21 "<h3>Hi!,</h3> <p>You've been subscribed to our primary newsletter. You can expect to receive an email from us every few weeks, sharing the new things that we've published and new houses to check out. Occasionally, We'll share unique newsletter-only content as well</p><p>Thanks for choosing us!</p>"
22 };
23 axios
24 .post(url, userData)
25 .then((res) => {
26 setSignup(res);
27 setSignupMsg(true);
28 setSignupLoading(false);
29 })
30 .catch((signupError) => {
31 setError(signupError);
32 setSignupLoading(false);
33 });
34 };
35
36 return { signup, signupError, signupMsg, signupLoading, handleChange, handleSignup };
37 };
38 export default usePost;
Let us import it in our Home file below:
1// frontend/src/pages/home/Home.js
1 import React from 'react';
2 import { Link } from 'react-router-dom';
3 import useFetch from '../../hooks/useFetch';
4 import usePost from '../../hooks/usePost';
5 import classes from './home.module.css';
6
7 export default function Homepage() {
8 const { estate, error, loading } = useFetch('http://localhost:1337/api/estates?populate=*');
9 const { signup, signupError, signupMsg, signupLoading, handleChange, handleSignup } = usePost(
10 'http://localhost:1337/api/email'
11 );
12
13 if (loading && signupLoading) return <p> Loading... </p>;
14 if (error) return <p> Error :( </p>;
15
16 return (
17 <div className={classes['home']}>
18 <section>
19 <h2>Welcome to our Estate</h2>
20 <hr className={classes['horizontal-rule']} />
21 <p>We help you find your new home</p>
22 <form className={classes['home-form']} onSubmit={handleSignup}>
23 <h5>Interested in joining our Newsletter</h5>
24 <h6>Sign up with your email below</h6>
25 <label htmlFor="email">
26 {signupError ? <p> {signupError} </p> : null}
27 Email Address:
28 <input type="email" name="email" value={signup} onChange={handleChange} />
29 {signupMsg ? <p> Thanks for signing up!</p> : null}
30 </label>
31 <button>Signup</button>
32 </form>
33 {estate.splice(0, 2).map((house) => (
34 <article className={classes['home-article']} key={house.id}>
35 <h2>{house.attributes.name}</h2>
36 <section className={classes['home-article-description']}>
37 <img
38 src={`http://localhost:1337${house.attributes.image.data[0].attributes.url}`}
39 alt="img"
40 />
41 <div>
42 <p>{house.attributes.price}</p>
43 <p>{house.attributes.description}</p>
44 <Link to={`estates/${house.id}`}>See More...</Link>
45 </div>
46 </section>
47 </article>
48 ))}
49 </section>
50 </div>
51 );
52 }
Go ahead and test your app.
We have seen how to use Strapi with React to build a startup website. We’ve also seen how to Integrate SendGrid with Strapi and React to create a newsletter email form.
Lastly, we have seen how to implement one of React’s new features— React Suspense — and what it does. I hope you understood what we did and can now implement same in your projects.
I am software developer skilled in JavaScript and React. I build things that run on the web and I write technical articles.