Optional chaining is a proposed feature that may be incorporated into the JavaScript specification.
The operator allows you to traverse through a nested object to get the value of variables without worrying if any of those will be undefined.
For example, without optional chaining, if you have the object below:
const person = {
name: 'Alice',
cat: {
name: 'Bob'
}
};
If you want to get the cat’s name, you have to use the code below:
const catName = person.cat.name;
If cat
is undefined or null in person
, the JavaScript interpreter will throw an error. With the optional chaining operator, you can write:
const catName = person?.cat?.name;
If cat
is undefined, the catName
will be null
.
It also works with keys of an object. Instead of const catName = person?.cat?.name;
, we can write:
const catName = person?.['cat']?.['name'];
This syntax also works with functions. For example, you can write:
func?.('foo')
To call the function func
with a string, where func
may be undefined or null. If the function does not exist, it will not be run.
To further illustrate the example and to show you how to use it in a real application, we will build a React app that uses the NASA API, to get the latest asteroid data.
We will use the Create React App CLI program to build the app.
As optional chaining is just a proposed feature, it is not currently supported with the CLI, so we have to do some work ourselves by installing some packages and making some changes to the Babel configuration of the app to enable optional chaining.
To start, we run npx create-react-app nasa-app
to create the project folder with the initial files.
Next, we install npm i -D @babel/plugin-proposal-optional-chaining customize-cra react-app-rewired
to begin customizing Create React App to support the optional chaining syntax.
Next, we have to add new configuration files and edit existing ones to let our app run and build with the syntax.
First, we add a file called config-overrides.js
and add the following:
const { useBabelRc, override, useEslintRc } = require("customize-cra");
module.exports = override(useBabelRc());
To let us use the .babelrc
configuration file.
Then, we create the .babelrc
file in the root folder of our project and add:
{
"plugins": [
[
"@babel/plugin-proposal-optional-chaining"
],
]
}
This will add support for optional chaining syntax in our project.
Next, we have to switch to make our app run and build with react-app-rewired
instead of the usual react-script
program.
To do this, in the scripts
section of package.json
, we put:
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test --env=jsdom",
"eject": "react-scripts eject"
}
In here, we replaced the original scripts that use react-script
to run and build our app, with react-app-rewired
.
Now we can use the optional chaining syntax to build our app.
First, we have to install some packages.
Run npm i axios bootstrap react-bootstrap formik yup react-router-dom
to install the axios HTTP client, React Bootstrap for styling, Formik and Yup for building forms and adding form validation, and React Router for routing URLs to the pages we build.
Now we can write some code. In App.js
, we replace the existing code with:
import React from "react";
import { Router, Route } from "react-router-dom";
import HomePage from "./HomePage";
import AsteroidsSearchPage from "./AsteroidsSearchPage";
import { createBrowserHistory as createHistory } from "history";
import "./App.css";
import TopBar from "./TopBar";
const history = createHistory();
function App() {
return (
<div className="App">
<Router history={history}>
<TopBar />
<Route path="/" exact component={HomePage} />
<Route path="/search" exact component={AsteroidsSearchPage} />
</Router>
</div>
);
}export default App;
So that we get client-side routing to our pages. In App.css
, we replace the existing code with:
.center {
text-align: center;
}
Next, we start building new pages. Create a file called AsteroidSearchPage.js
in the src
folder and add:
import React, { useState, useEffect } from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import * as yup from "yup";
import Card from "react-bootstrap/Card";
import "./AsteroidsSearchPage.css";
import { searchFeed } from "./requests";
const schema = yup.object({
startDate: yup
.string()
.required("Start date is required")
.matches(
/([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/,
"Invalid start date"
),
endDate: yup
.string()
.required("End date is required")
.matches(
/([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|\[12]\d|3\[01]))/,
"Invalid end date"
),
});
function AsteroidsSearchPage() {
const [feed, setFeed] = useState({});
const [error, setError] = useState("");
const handleSubmit = async evt => {
const isValid = await schema.validate(evt);
if (!isValid) {
return;
}
try {
const response = await searchFeed(evt);
setFeed(response.data.near_earth_objects);
} catch (ex) {
alert(ex?.response?.data?.error_message);
}
};
return (
<div className="AsteroidsSearchPage">
<h1 className="center">Search Asteroids</h1>
<Formik validationSchema={schema} onSubmit={handleSubmit}>
{({
handleSubmit,
handleChange,
handleBlur,
values,
touched,
isInvalid,
errors,
}) => (
<Form noValidate onSubmit={handleSubmit}>
<Form.Row>
<Form.Group as={Col} md="12" controlId="startDate">
<Form.Label>Start Date</Form.Label>
<Form.Control
type="text"
name="startDate"
placeholder="YYYY-MM-DD"
value={values.startDate || ""}
onChange={handleChange}
isInvalid={touched.startDate && errors.startDate}
/>
<Form.Control.Feedback type="invalid">
{errors.startDate}
</Form.Control.Feedback>
</Form.Group>
<Form.Group as={Col} md="12" controlId="endDate">
<Form.Label>End Date</Form.Label>
<Form.Control
type="text"
name="endDate"
placeholder="YYYY-MM-DD"
value={values.endDate || ""}
onChange={handleChange}
isInvalid={touched.startDate && errors.endDate}
/><Form.Control.Feedback type="invalid">
{errors.endDate}
</Form.Control.Feedback>
</Form.Group>
</Form.Row>
<Button type="submit" style={{ marginRight: "10px" }}>
Search
</Button>
</Form>
)}
</Formik>
{Object.keys(feed)?.map(key => {
return (
<Card style={{ width: "90vw", margin: "0 auto" }}>
<Card.Body>
<Card.Title>{key}</Card.Title>
{feed?.[key]?.length > 0
? feed?.[key]?.map(f => {
return (
<div style={{ paddingBottom: "10px" }}>
{f?.close_approach_data?.length > 0 ? (
<div>
<b>
Close Approach Date:
{f?.close_approach_data?.map(d => {
return <p>{d?.close_approach_date_full}</p>;
})}
</b>
</div>
) : null}
<div>
Minimum Estimated Diameter:{" "}
{
f?.estimated_diameter?.kilometers
?.estimated_diameter_min
}{" "}
km
</div>
<div>
Maximum Estimated Diameter:{" "}
{
f?.estimated_diameter?.kilometers
?.estimated_diameter_max
}{" "}
km
<br />
</div>
</div>
);
})
: null}
</Card.Body>
</Card>
);
})}
</div>
);
}
export default AsteroidsSearchPage;
This is where we add a form to search for asteroid data from the NASA API by date range. Both the start and end date fields should be in YYYY-MM-DD format and we changed our form validation to match that in the schema
object.
Once validation is done, by calling the schema.validate
function, we search. The response has many nested objects, so we use the optional chaining syntax everywhere in the result cards.
We loop through close_approach_data
array, and we don’t assume it’s always defined or has a greater than zero length, the same goes for the call to the map
function. We do not assume that the map
function is always defined.
We also use the optional chaining syntax for f?.estimated_diameter?.kilometers ?.estimated_diameter_min
and f?.estimated_diameter?.kilometers ?.estimated_diameter_max
.
The more levels of nesting there are, the less likely it is that you can traverse the object tree successfully without the optional chaining syntax, as there is more chance of nested objects being undefined.
Also, note that the optional chaining syntax works for returned results like Object.keys(feed)
In AsteroidSearchPage.css
, which we should create in the src
folder, we put:
.AsteroidsSearchPage{
margin: 0 auto;
width: 90vw;
}
To add some margins to the page.
Next, we build the home page. Create a file called HomePage.js
in the src
folder and add:
import React, { useState, useEffect } from "react";
import { browse } from "./requests";
import Card from "react-bootstrap/Card";
import "./HomePage.css";
function HomePage() {
const [initialized, setIntialized] = useState(false);
const [feed, setFeed] = useState([]);
const browserFeed = async () => {
const response = await browse();
setFeed(response.data.near_earth_objects);
setIntialized(true);
};
useEffect(() => {
if (!initialized) {
browserFeed();
}
});
return (
<div className="HomePage">
<h1 className='center'>Asteroids Close to Earth</h1>
<br />
{feed?.map(f => {
return (
<Card style={{ width: "90vw", margin: "0 auto" }}>
<Card.Body>
<Card.Title>{f?.name}</Card.Title>
<div>
{f?.close_approach_data?.length > 0 ? (
<div>
Close Approach Date:
{f?.close_approach_data?.map(d => {
return <p>{d?.close_approach_date_full}</p>;
})}
</div>
) : null}
<p>
Minimum Estimated Diameter:{" "}
{f?.estimated_diameter?.kilometers?.estimated_diameter_min} km
</p>
<p>
Maximum Estimated Diameter:{" "}
{f?.estimated_diameter?.kilometers?.estimated_diameter_max} km
</p>
</div>
</Card.Body>
</Card>
);
})}
</div>
);
}
export default HomePage;
This page is very similar to the AsteroidSearchPage
component with the use of the optional chaining syntax.
Next, create HomePage.css
in the src
folder and add:
.HomePage{
text-align: left;
}
To align our text to the left.
Next, we create requests.js
in the src
folder and add:
const APIURL = "https://api.nasa.gov/neo/rest/v1]";
const axios = require("axios");
export const searchFeed = data =>
axios.get(
`${APIURL}/feed?start_date=${data.startDate}&end_date=${data.endDate}&api_key=${process.env.REACT_APP_APIKEY}`
);
export const browse = () =>
axios.get(`${APIURL}/neo/browse?api_key=${process.env.REACT_APP_APIKEY}`);
To add the functions for making the HTTP requests to the NASA API for getting asteroid data and searching for them.
process.env.REACT_APP_APIKEY
has the API key when you add the API key to the .env
file of your project with REACT_APP_APIKEY
as the key. Register for an API key at NASA.
Finally, we add TopBar.js
to the src
folder and add:
import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import { withRouter } from "react-router-dom";
function TopBar({ location }) {
const { pathname } = location;
return (
<Navbar bg="primary" expand="lg" variant="dark">
<Navbar.Brand href="#home">NASA App</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="mr-auto">
<Nav.Link href="/" active={pathname == "/"}>
Home
</Nav.Link>
<Nav.Link href="/search" active={pathname.includes("/search")}>
Search
</Nav.Link>
</Nav>
</Navbar.Collapse>
</Navbar>
);
}
export default withRouter(TopBar);
This is the navigation bar at the top of each page in our app. We set the active
prop by checking the current URL of the page so we get highlights in our links.