Follow this tutorial to get a React project deployed in Heroku, and running locally in your machine during development.
If you are new to GitHub, Git, or Heroku, please follow this tutorial first.
Check the currently deployed version: https://swe432-heroku-react.herokuapp.com.
Part 1 available here.
Parts 2 and 3 available here.
There are several ways to use React, or any front-end framework, for use in your web apps or talk about code. Depending on your goal, your setup may be different from what others use:
-
Getting started code: uses hardcoded links to manage packages, quick to try libraries, yet, is not intended for production.
-
Shareable executable code (AKA Pastebin): manages the environment for you (packages and project), and allows others to see your work online, however, they are mostly for conversations not for production.
-
Production-ready code (this repo): the developer controls the app development and deployment, the app is within an environment where the packages and projects are managed by other software, as is expected in projects within companies. Next class will be about this setup type.Â
This project aims to showcase a Single Page App (SPA) built using React, React offers a zero configuration development environment to do so: Create React App (CRA). This project already has set up a CRA, so you can build upon it.
CRA uses NPM to manage the project, Babel to compile/transpile your code, and Webpack to bundle your app. You do not have to worry about Babel or Webpack, CRA handles them (and other packages) for you.
Think of NPM as Maven for JavaScript, a platform that manages your project dependencies based on a configuration, NPM's package.json is equivalent to Maven's POM.xml. NPM supports scripting so Babel and Webpack can be executed to build your app with CRA.
- Create a new empty repo. Get its URL,
YOUR_NEW_REPO_URL
:
https://github.com/YOUR_USERNAME/YOUR_NEW_REPO_NAME.git
Note: We will also use YOUR_NEW_REPO_NAME
later on.
-
Locate a directory where you want to clone this repo.
-
Now replace
YOUR_NEW_REPO_URL
with your new repo's URL in the following commands, and run them:
git clone https://github.com/luminaxster/swe432-heroku-react.git
cd swe432-heroku-react
git init && git remote set-url origin "YOUR_NEW_REPO_URL"
git push
- Install NPM, it comes bundled with Node.js.
- Locate the
swe432-heroku-react
folder and run these commands:
npm install
npm run start-dev
Your local app should be running at localhost:3000
.
Use npm run start-dev
to run your app locally and let it running, it will watch for changes in your code and it will automatically re-bundle your app. You can refresh your browser tab and observe your latest changes.
In the case of npm install
, you only have to run it when you add packages to your package.json
.
If you get a cryptic error like throw er; // Unhandled 'error' event
during npm install
, try removing the package-lock.json file and the node_modules folder, then try re-running the command.
Alternatively, open a terminal, locate your project's root folder, and run these commands:
rm package-lock.json
rm yarn.lock
rm -rf ./node_modules
npm install
rm package-lock.json
rm yarn.lock
rd -r .\node_modules
npm install
This is section is similar (not equal) to what we did for the servlet project, the goal is to link this GitHub repo to Heroku so when you push your changes to the repo Heroku will detect those changes and redeploy your app.
- In the main page, create a new app.
Go to the app's settings tab:
- In Buildpacks, add a new buildpack, and save changes after entering the following buildpack URL:
https://github.com/mars/create-react-app-buildpack.git
Go to the app's deploy tab:
- In Deploy method, choose
GitHub
. - In Connect to GitHub, type your new repo name(
YOUR_NEW_REPO_NAME
) and search for it, and connect to it from the list. - In Automatic deploys, enable automatic deploy.
- In Manual deploy, deploy the
main
(ormaster
if applicable) branch. - A build log will appear, wait until it finishes building and deploying the app.
- In Deploy to Heroku, a view option will appear, it will open a browser tab with the public URL of the app.
Your React app is now deployed.
Heroku and Github can be integrated to automatically deploy your app (above). However, an Unauthorized
error notification may show up in the dashboard. This issue has been reported by Heroku (Thanks! anonyomous student from the spring 2022 class).
The solution is to use Heroku's Git service instead of GitHub's:
-
Open a terminal and go to your GitHub repo's folder. Unix-like users should use Terminal (MacOS), and Windows users CMD.
-
Check you are at the right location:
git status
You will receive a message starting with On branch...
, otherwise, you are not on the right folder or Git has not been configured correctly.
- Log into your Heroku account if you haven't:
heroku login
It will ask permission to open a browser tab to log you in. Return to the terminal once logged in.
- Obtain the name,
YOUR_HEROKU_APP_NAME
, of the Heroku app you want to migrate:
heroku apps
You will see your account email and a list of the names of the apps you have access to.
- Add Heroku Git as another repository to your chosen app's Git configuration:
heroku git:remote -a YOUR_HEROKU_APP_NAME
Remember to replace YOUR_HEROKU_APP_NAME
with the name of the app as shown in step 4.
- Check the effects of the previous commmand:
git remote -v
There should be two lines starting with heroku
. Now you can pull and push changes to your Heroku Git repo, in addtion to your existing GitHub one.
- Push your changes to the new repo to deploy your app:
git add .
git commit -am "Heroku's Git repo now available."
git push heroku
This step takes a while. It will show you the progress of your current deployment. If the deployment succedes, you will a line like this at the end: remote: https://YOUR_HEROKU_APP_NAME.herokuapp.com/ deployed to Heroku
.
Your React app is now deployed.
Note: Your GitHub repo is working as it should, we added support to do the same in Heroku. While the integration issue is open, please commit your changes to both repos: GitHub's to manage your codebase, and Heroku's to deploy your app's latest version.
If during deployment, your logs show an error like npm ERR! Cannot read property 'match' of undefined
, you need to clean your Heroku server's cache, run these commands:
heroku plugins:install heroku-repo
heroku repo:purge_cache -a YOUR_HEROKU_APP_NAME
Don't forget to replace YOUR_HEROKU_APP_NAME
with your app's name, now redeploy your app.
If the error shows up again, only use heroku repo:purge_cache -a YOUR_HEROKU_APP_NAME
.
More details here. To stop this issue from keep happening make sure step 2 change is set in your Heroku app.
Alternatively, you can clear the app's cache in your Heroku dashboard. Go to the app's settings tab In Config Vars, reveal them, and add the key NODE_MODULES_CACHE
with value false
.
In the following examples, front-end (this React project) components fetch data asynchronously from back-end Tomcat servlet microservices.
For simplicity, the Tomcat and React Heroku projects are in separate Heroku apps.
The deployed solution is available: back-end service, and front-end client( Popcorn Sales tab).
a.
@WebServlet(name = "zipLookup", urlPatterns = {"/zipLookup"})
Maps the URL yourwebsite.com/zipLookup like in https://swe432tomcat.herokuapp.com/zipLookup
.
b.
@Override
protected void doGet (HttpServletRequest req, HttpServletResponse res) ...
Standard servlet method for handling GET requests.
c.
out.print(new Gson().toJson(data));
Sends data
back to the client as a JSON formatted string like "{"key":"value"}"
.
d.
useEffect(() => {
setZipError(null);
if (addressZip.length > 4 && !addressState && !addressCity) {
getPlace(zipURL, addressZip, getPlaceSuccessCallback, getPlaceFailureCallback, library);
}
}, [ // dependency array
zipURL,
library,
addressZip,
getPlaceSuccessCallback,
getPlaceFailureCallback,
addressState,
addressCity
]);
The React hook useEffect
will be trigger every time any of the references in the dependency array changes, including the first time they are assigned.
e.
function getPlaceFetch(
url,
zip,
thenCallback,
catchCallback,
) {
// Call the response software component
const query = url + "?zip=" + zip;
fetch(query)
.then(
// Register the embedded handler function
// This function will be called when the server returns
// (the "callback" function)
// 4 means finished, and 200 means okay.
(response) => {
// Data should look like "Fairfax, Virginia"
response.json().then(thenCallback);
}
)
.catch(catchCallback);
}
By default, this is the function getPlace(...)
is called within the hook, it is asynchronous so when is called it does not block the UI until it finishes its payload, instead, it handles a promise, a wrapping object that executes the function in the background and offers a method to know when it is done. This method is then(), to which you can pass a function that will be called back once the promise finishes.
Fetch does what HTML's form action does internally, in this case, we are configuring a call to the back-end microservice, with the same configuration as a form action, and sending the values manually in the request's query
("?zip=" + zip
). Previously, these values were contained within the form and sent after typing the zip code.
f.
fetch(query)
After making the call to the server at a., b. will start processing it, and responds with c.. Then at front-end, it will be received and held in this promise.
g.
.then((response) => {
// Data should look like "Fairfax, Virginia"
response.json().then(thenCallback);
})
Once the fetch promises is completed, response.json()
returns a promise with JSON data created in c., then the result of then()
(pun intended) will be assigned as the first parameter of the thenCallback
function and be called, which should have the value {city:"aCity",state:"aState"}
, based on what was sent in (f.)
h.
const getPlaceSuccessCallback = useCallback((result) => {
const { city, state } = result;
if (state && city) {
setAddressState(state);
setAddressCity(city);
}
}, []);
Hint: The getPlaceSuccessCallback
function was passed in (d.) to be used as the thenCallback
in (g.).
This will update the state of the variables addressState
and addressState
to what result
is referencing, thus, initiating a re-render the component PopcornSales
.
i.
const [addressState, setAddressState] = useState("");
const [addressCity, setAddressCity] = useState("");
During rendering of the functional Component PopcornSales
, the useState hooks set addressState
and addressCity
to what (h.) processed.
Note: that ""
value assigned only the first time the component is rendered.
j.
<tr>
<td> City: </td>
<td>
<input
type="text"
name="city"
id="city"
size="30"
value={addressCity}
onChange={handleAddressCityChange}
/>
</td>
</tr>
<tr>
<td> State: </td>
<td>
<input
type="text"
name="state"
id="state"
size="30"
value={addressState}
onChange={handleAddressStateChange}
/>
</td>
</tr>
Now the city and state input
elements will show the respective values to the user thanks to value={addressCity}
and value={addressState}
.
The deployed solution is available: back-end service, and front-end client( Fetcher tab).
a.
@WebServlet(name = "EchoServlet", urlPatterns = {"/echo"})
Maps the URL yourwebsite.com/echo like in https://swe432tomcat.herokuapp.com/echo
.
b.
@Override
protected void doPost (HttpServletRequest req, HttpServletResponse res) ...
Standard servlet method for handling POST requests.
c.
out.print(new Gson().toJson(data));
Sends data
back to the client as a JSON formatted string like "{"key":"value"}"
.
d.
useEffect( ()=> {... fetchData().catch(e=>{console.log(e)});}, [url, clicks]);
This React hook will be trigger every time url
or clicks
change, including the first time they are assigned, afterwards, it will be triggered every time the submit button is clicked.
e.
const fetchData= async()=>{...}
This is the function called within the hook, it is asynchronous so when is called it does not block the UI until it finishes its payload, instead, it returns a promise, a wrapping object that executes the function in the background and offers a method to know when it is done. This method is then(), to which you can pass a function that will be called back once the promise finishes. We did not use the then() method directly, but catch(), which works the same way but when there is an error. In d. we passed one arrow function to catch when there is an error.
f.
const res = await fetch('https://swe432tomcat.herokuapp.com/echo', {method:'POST', mode: 'cors', headers:{'Content-Type': 'application/x-www-form-urlencoded'}, body:'key1=value1&kay2=value2'}
Fetch does what HTML's form action does internally, in this case, we are configuring a call to the back-end microservice, with the same configuration as a form action, and sending the values manually in the request's body
. Previously, these values were contained within the form and sent after clicking submit.
Await is a short-hand for resolving fetch(...).then(), so it will make the call to the server(at a.) (b. will start handling it), and wait until it responds (c. finishes handling it) and comes back to the client, thus assigned to res
.
g.
const json = await res.json();
json
will be assigned the JSON data assembled in c., where res.json() returns a promise, and similarly to d., the result of then() will be assigned to the variable json, which should have the value {key1:"value1",key2:"value2"}, based on what was sent in body
(f.)
h.
setResponse(json);
This will update the state of the variable response
to what what json
is referencing, thus, initiating a re-render the component Fetcher.
i.
const [response, setResponse] = useState(null);
This will re-run the functional Component Fetcher, now the useState hook has set response to {key1:"value1",key2:"value2"}
. Note that null
is assigned only the first time the component is executed.
j.
response?JSON.stringify(response):...
Since response has {key1:"value1",key2:"value2"}
, response?
evaluate to true
, thus printing the following {"key1":"value1","key2":"value2"}
in the browser.
Depending on your app's values you can decide which components to show. e.g.:
let Component = ()=>{.... return condition?<ComponentA/>: <ComponentB/>;}// you can return null, too.
Similar to Example 2's step j, you can use response or another variable to keep track if the user clicks the submit button and conditionally render components like a form or result list. These are the relevant statements:
function YourComponent(){
...
const [isSubmitted, setIsSubmitted] = useState(false);
....
return isSubmitted? <ResultComponent ... />:
<FormComponent...>
...
<button onClick={()=>{fetch(...)... setIsSubmitted(true)}}>submit</button>
</FormComponent>;
}
Creating a React app for Hook from scratch