I'm in 💖 with the JAMStack, It gets the work done. One of the very exciting companies in this area is Netlify. Anyone who tested their hosting would tell you it's top class and I would recommend it anyday.
In this post we will explore using their Serverless functions with create-react-app.
What we intend to create a reading application. You give it the URL and allows you to view the simplified content fit for reading.
We ideally to parse the url string from the backend to avoid getting blocked by CORS. We will use a Netlify Function to achieve this. We will use Postlight's Mercury Parser with the function to parse the simplified version from URL.
First let's create a new React application with create-react-app:
npm init react-app the-reader
Now, to setup Netlify functions, create a top level folder, I'm naming it functions
. We have to update the build step so that the function is also build when we run yarn build
.
Netlify has published a package netlify-lambda to help with the build:
yarn add netlify-lambda npm-run-all --dev
npm-run-all
is used to run both tasks in parallel. In package.json
:
"scripts": {
"build": "run-p build:**",
"build:app": "react-scripts build",
"build:lambda": "netlify-lambda build functions/",
}
Create a netlify.toml
so that netlify
knows where the build is:
[build]
command = "yarn build"
functions = "build-lambda" # netlify-lambda gets build to this folder
publish = "build" # create-react-app builds to this folder
Remember to add
build-lambda
to.gitignore
.
Create your first function by creating a JS file in functions
folder we created earlier.
Netlify can recognise JS files in the folder or in nested folders.
In functions/parse.js
:
export async function handler(event) {
return {
statusCode: 200,
body: JSON.stringify({ data: "hello world" }),
};
}
From the frontend application you can now use fetch
to query .netlify/functions/parse.js
(your folder structure prepended with .netlify/
) to get the dummy response we put in. But with a twist, it works only when you deploy the application to Netlify. That's not a good development methodology. This is because the functions are not build yet and there is .netlify/
path to get the data from.
netlify-lambda
has a serve mode for development, so that the functions can be build for any changes and updated to a server.
Add the following to package.json
and keep it running in the background with npm start
:
"scripts": {
"serve:lambda": "netlify-lambda serve functions/",
},
You will find that the functions is now running on a server with localhost:9000
. But even if you could add an environment variable to query this server, there is an issue with CORS now. Your frontend and functions are running on different servers. To get around this, you can add a proxy with create-react-app
. You can find complete instructions in the docs.
What we have to do is to add src/setupProxy.js
, you don't have to import this file anywhere, just create, add code and ✨ restart your development server.
const proxy = require("http-proxy-middleware");
module.exports = function (app) {
app.use(
proxy("/.netlify/functions/", {
target: "http://localhost:9000/",
pathRewrite: {
"^/\\.netlify/functions": "",
},
}),
);
};
What this is essentially doing is to rewrite any API calls to .netlify/functions
to localhost:9000
and get response from there. This only works in development, so it works without the server in production.
First, let's setup a form where user can enter a URL and request the server.
import React from "react";
const App = () => {
const handleSubmit = () => {};
return (
<main>
<form onSubmit={handleSubmit}>
<input type="url" placeholder="Enter url here" name="url" label="url" />
<button>View</button>
</form>
</main>
);
};
Filling in the handleSubmit
function:
import { stringify } from "qs";
// for encoding the URL as a GET parameter
const handleSubmit = (event) => {
event.preventDefault();
const url = event.target.url.value;
fetch(`/.netlify/functions/parse?${stringify({ q: reqUrl })}`).then(
(response) => response.json(),
);
};
If you run this function now, it will return the { data: "Hello world" }
we added earlier (hopefully).
To return some real data, let's modify the functions/parse.js
to:
import Mercury from "@postlight/mercury-parser";
export async function handler(event) {
const parameters = event.queryStringParameters;
const url = parameters.q;
if (!url) {
return {
statusCode: 400,
body: JSON.stringify({ error: "Invalid/No URL provided" }),
};
}
try {
const response = await Mercury.parse(url);
return {
statusCode: 200,
body: JSON.stringify({ data: response }),
};
} catch (err) {
return {
statusCode: 500,
body: JSON.stringify({ error: err }),
};
}
}
The function takes URL as an argument through queryStringParameters
and uses Mercury.parse
to get the simplified version and return it to the user.
Now, running the frontend would get you the real response from the serverless function (which underwhelmingly has a server now, but you can always push and get it deployed).
Some changes on the frontend to display the data from backend:
import { stringify } from "qs";
import React, { useState } from "react";
const App = () => {
const [result, setResult] = useState(null);
const handleSubmit = (event) => {
event.preventDefault();
const url = event.target.url.value;
fetch(`/.netlify/functions/parse?${stringify({ q: reqUrl })}`)
.then((response) => response.json())
.then((jsonResponse) => setResult(jsonResponse.data));
};
return (
<main>
<form onSubmit={handleSubmit}>
<input type="url" placeholder="Enter url here" name="url" label="url" />
<button>View</button>
</form>
{result && <article dangerouslySetInnerHTML={{ __html: data.content }} />}
</main>
);
};
and we are Done 🥂.
To convert this to a PWA, you can very simply add the service workers on the create-react-app
and adjust the parameters in manifest.json
.
It's too much of work to actually copy the URL and copy paste into our app, especially on mobile. But for PWAs what we can do is to register the application as a share target. You can do this by specifying the following in manifest.json
:
"share_target": {
"action": "/",
"method": "GET",
"params": {
"text": "q"
}
}
The important thing here is the params
object. On the left is what is to be passed, there are different params like title
, text
and url
. For some wierd reason, Chrome sends the URL of the page in the text
parameter and that is what we are substituting here.
When something is shared to the application, the URL would be:
https://baseurl.com?q=shared-url
To process this, the frontend can:
const location = useLocation();
useEffect(() => {
const searchParams = new URLSearchParams(location.search);
const url = searchParams.get("q");
if (url) {
handleView(url);
}
}, [location.search]);
You can find the complete code in the repository