Build a React app that use AEM’s GraphQL APIs
In this chapter, you explore how AEM’s GraphQL APIs can drive the experience in an external application.
A simple React app is used to query and display Team and Person content exposed by AEM’s GraphQL APIs. The use of React is largely unimportant, and the consuming external application could be written in any framework for any platform.
Prerequisites
It is assumed that the steps outlined in the previous parts of this multi-part tutorial have been completed, or basic-tutorial-solution.content.zip is installed on your AEM as a Cloud Service Author and Publish services.
IDE screenshots in this chapter come from Visual Studio Code
The following software must be installed:
Objectives
Learn how to:
- Download and start the example React app
- Query AEM’s GraphQL end-points using the AEM Headless JS SDK
- Query AEM for a list of teams, and their referenced members
- Query AEM for a team member’s details
Get the sample React app
In this chapter, a stubbed-out sample React app is implemented with the code required to interact with AEM’s GraphQL API, and display team and person data obtained from them.
The sample React app source code is available on Github.com at https://github.com/adobe/aem-guides-wknd-graphql/tree/main/basic-tutorial
To get the React app:
-
Clone the sample WKND GraphQL React app from Github.com.
code language-shell $ cd ~/Code $ git clone git@github.com:adobe/aem-guides-wknd-graphql.git
-
Navigate to
basic-tutorial
folder and open it in your IDE.code language-shell $ cd ~/Code/aem-guides-wknd-graphql/basic-tutorial $ code .
-
Update
.env.development
to connect to AEM as a Cloud Service Publish service.- Set
REACT_APP_HOST_URI
’s value to be your AEM as a Cloud Service’s Publish URL (ex.REACT_APP_HOST_URI=https://publish-p123-e456.adobeaemcloud.com
) andREACT_APP_AUTH_METHOD
’s value tonone
note note NOTE Make sure you have published project configuration, Content Fragment models, authored Content Fragments, GraphQL endpoints and persisted queries from previous steps. If you performed above steps on local AEM Author SDK, you can point to http://localhost:4502
andREACT_APP_AUTH_METHOD
’s value tobasic
. - Set
-
From the command line, go to the
aem-guides-wknd-graphql/basic-tutorial
folder -
Start the React app
code language-shell $ cd ~/Code/aem-guides-wknd-graphql/basic-tutorial $ npm install $ npm start
-
The React app starts in development mode on http://localhost:3000/. Changes made to the React app throughout the tutorial are reflected immediately.
Anatomy of the React app
The sample React app has three main parts:
-
The
src/api
folder contains files used to make GraphQL queries to AEM.src/api/aemHeadlessClient.js
initializes and exports the AEM Headless Client used to communicate with AEMsrc/api/usePersistedQueries.js
implements custom React hooks return data from AEM GraphQL to theTeams.js
andPerson.js
view components.
-
The
src/components/Teams.js
file displays a list of teams and their members, by using a list query. -
The
src/components/Person.js
file displays the details of a single person, using a parameterized, single-result query.
Review the AEMHeadless object
Review the aemHeadlessClient.js
file for how to create the AEMHeadless
object used to communicate with AEM.
-
Open
src/api/aemHeadlessClient.js
. -
Review the lines 1-40:
-
The import
AEMHeadless
declaration from the AEM Headless Client for JavaScript, line 11. -
The configuration of authorization based on variables defined in
.env.development
, line 14-22, and, the arrow function expressionsetAuthorization
, line 31-40. -
The
serviceUrl
setup for the included development proxy configuration, line 27.
-
-
Lines 42-49, are most important, as they instantiate the
AEMHeadless
client and export it for use throughout the React app.
// Initialize the AEM Headless Client and export it for other files to use
const aemHeadlessClient = new AEMHeadless({
serviceURL: serviceURL,
endpoint: REACT_APP_GRAPHQL_ENDPOINT,
auth: setAuthorization(),
});
export default aemHeadlessClient;
Implement to run AEM GraphQL persisted queries
To implement the generic fetchPersistedQuery(..)
function to run the AEM GraphQL persisted queries open the usePersistedQueries.js
file. The fetchPersistedQuery(..)
function uses the aemHeadlessClient
object’s runPersistedQuery()
function to run query asynchronously, promise-based behavior.
Later, custom React useEffect
hook calls this function to retrieve specific data from AEM.
- In
src/api/usePersistedQueries.js
updatefetchPersistedQuery(..)
, line 35, with the code below.
/**
* Private, shared function that invokes the AEM Headless client.
*
* @param {String} persistedQueryName the fully qualified name of the persisted query
* @param {*} queryParameters an optional JavaScript object containing query parameters
* @returns the GraphQL data or an error message
*/
async function fetchPersistedQuery(persistedQueryName, queryParameters) {
let data;
let err;
try {
// AEM GraphQL queries are asynchronous, either await their return or use Promise-based syntax
const response = await aemHeadlessClient.runPersistedQuery(
persistedQueryName,
queryParameters
);
// The GraphQL data is stored on the response's data field
data = response?.data;
} catch (e) {
// An error occurred, return the error messages
err = e
.toJSON()
?.map((error) => error.message)
?.join(", ");
console.error(e.toJSON());
}
// Return the GraphQL and any errors
return { data, err };
}
Implement Teams functionality
Next, build out the functionality to display the Teams and their members on the React app’s main view. This functionality requires:
- A new custom React useEffect hook in
src/api/usePersistedQueries.js
that invokes themy-project/all-teams
persisted query, returning a list of Team Content Fragments in AEM. - A React component at
src/components/Teams.js
that invokes the new custom ReactuseEffect
hook, and renders the teams data.
Once complete, the app’s main view populates with the teams data from AEM.
Steps
-
Open
src/api/usePersistedQueries.js
. -
Locate the function
useAllTeams()
-
To create a
useEffect
hook that invokes the persisted querymy-project/all-teams
viafetchPersistedQuery(..)
, add the following code. The hook also only returns the relevant data from the AEM GraphQL response atdata?.teamList?.items
, allowing the React view components to be agnostic of the parent JSON structures.code language-javascript /** * Custom hook that calls the 'my-project/all-teams' persisted query. * * @returns an array of Team JSON objects, and array of errors */ export function useAllTeams() { const [teams, setTeams] = useState(null); const [error, setError] = useState(null); // Use React useEffect to manage state changes useEffect(() => { async function fetchData() { // Call the AEM GraphQL persisted query named "my-project/all-teams" const { data, err } = await fetchPersistedQuery( "my-project/all-teams" ); // Sets the teams variable to the list of team JSON objects setTeams(data?.teamList?.items); // Set any errors setError(err); } // Call the internal fetchData() as per React best practices fetchData(); }, []); // Returns the teams and errors return { teams, error }; }
-
Open
src/components/Teams.js
-
In the
Teams
React component, fetch the list of teams from AEM using theuseAllTeams()
hook.code language-javascript import { useAllTeams } from "../api/usePersistedQueries"; ... function Teams() { // Get the Teams data from AEM using the useAllTeams const { teams, error } = useAllTeams(); ... }
-
Perform the view-based data validation, displaying an error message or loading indicator based on the returned data.
code language-javascript function Teams() { const { teams, error } = useAllTeams(); // Handle error and loading conditions if (error) { // If an error ocurred while executing the GraphQL query, display an error message return <Error errorMessage={error} />; } else if (!teams) { // While the GraphQL request is executing, show the Loading indicator return <Loading />; } ... }
-
Finally, render the teams data. Each team returned from the GraphQL query is rendered using the provided
Team
React subcomponent.code language-javascript import React from "react"; import { Link } from "react-router-dom"; import { useAllTeams } from "../api/usePersistedQueries"; import Error from "./Error"; import Loading from "./Loading"; import "./Teams.scss"; function Teams() { const { teams, error } = useAllTeams(); // Handle error and loading conditions if (error) { return <Error errorMessage={error} />; } else if (!teams) { return <Loading />; } // Teams have been populated by AEM GraphQL query. Display the teams. return ( <div className="teams"> {teams.map((team, index) => { return <Team key={index} {...team} />; })} </div> ); } // Render single Team function Team({ title, shortName, description, teamMembers }) { // Must have title, shortName and at least 1 team member if (!title || !shortName || !teamMembers) { return null; } return ( <div className="team"> <h2 className="team__title">{title}</h2> <p className="team__description">{description.plaintext}</p> <div> <h4 className="team__members-title">Members</h4> <ul className="team__members"> {/* Render the referenced Person models associated with the team */} {teamMembers.map((teamMember, index) => { return ( <li key={index} className="team__member"> <Link to={`/person/${teamMember.fullName}`}> {teamMember.fullName} </Link> </li> ); })} </ul> </div> </div> ); } export default Teams;
Implement Person functionality
With the Teams functionality complete, let’s implement the functionality to handle the display on a team member’s, or person’s, details.
This functionality requires:
-
A new custom React useEffect hook in
src/api/usePersistedQueries.js
that invokes the parameterizedmy-project/person-by-name
persisted query, and returns a single person record. -
A React component at
src/components/Person.js
that uses a person’s full name as a query parameter, invokes the new custom ReactuseEffect
hook, and renders the person data.
Once complete, selecting a person’s name in the Teams view, renders the person view.
-
Open
src/api/usePersistedQueries.js
. -
Locate the function
usePersonByName(fullName)
-
To create a
useEffect
hook that invokes the persisted querymy-project/all-teams
viafetchPersistedQuery(..)
, add the following code. The hook also only returns the relevant data from the AEM GraphQL response atdata?.teamList?.items
, allowing the React view components to be agnostic of the parent JSON structures.code language-javascript /** * Calls the 'my-project/person-by-name' and provided the {fullName} as the persisted query's `name` parameter. * * @param {String!} fullName the full * @returns a JSON object representing the person */ export function usePersonByName(fullName) { const [person, setPerson] = useState(null); const [errors, setErrors] = useState(null); useEffect(() => { async function fetchData() { // The key is the variable name as defined in the persisted query, and may not match the model's field name const queryParameters = { name: fullName }; // Invoke the persisted query, and pass in the queryParameters object as the 2nd parameter const { data, err } = await fetchPersistedQuery( "my-project/person-by-name", queryParameters ); if (err) { // Capture errors from the HTTP request setErrors(err); } else if (data?.personList?.items?.length === 1) { // Set the person data after data validation setPerson(data.personList.items[0]); } else { // Set an error if no person could be found setErrors(`Cannot find person with name: ${fullName}`); } } fetchData(); }, [fullName]); return { person, errors }; }
-
Open
src/components/Person.js
-
In the
Person
React component, parse thefullName
route parameter, and fetch the person data from AEM using theusePersonByName(fullName)
hook.code language-javascript import { useParams } from "react-router-dom"; import { usePersonByName } from "../api/usePersistedQueries"; ... function Person() { // Read the person's `fullName` which is the parameter used to query for the person's details const { fullName } = useParams(); // Query AEM for the Person's details, using the `fullName` as the filtering parameter const { person, error } = usePersonByName(fullName); ... }
-
Perform view-based data validation, displaying an error message or loading indicator based on the returned data.
code language-javascript function Person() { // Read the person's `fullName` which is the parameter used to query for the person's details const { fullName } = useParams(); // Query AEM for the Person's details, using the `fullName` as the filtering parameter const { person, error } = usePersonByName(fullName); // Handle error and loading conditions if (error) { return <Error errorMessage={error} />; } else if (!person) { return <Loading />; } ... }
-
Finally, render the person data.
code language-javascript import React from "react"; import { useParams } from "react-router-dom"; import { usePersonByName } from "../api/usePersistedQueries"; import { mapJsonRichText } from "../utils/renderRichText"; import Error from "./Error"; import Loading from "./Loading"; import "./Person.scss"; function Person() { // Read the person's `fullName` which is the parameter used to query for the person's details const { fullName } = useParams(); // Query AEM for the Person's details, using the `fullName` as the filtering parameter const { person, error } = usePersonByName(fullName); // Handle error and loading conditions if (error) { return <Error errorMessage={error} />; } else if (!person) { return <Loading />; } // Render the person data return ( <div className="person"> <img className="person__image" src={process.env.REACT_APP_HOST_URI+person.profilePicture._path} alt={person.fullName} /> <div className="person__occupations"> {person.occupation.map((occupation, index) => { return ( <span key={index} className="person__occupation"> {occupation} </span> ); })} </div> <div className="person__content"> <h1 className="person__full-name">{person.fullName}</h1> <div className="person__biography"> {/* Use this utility to transform multi-line text JSON into HTML */} {mapJsonRichText(person.biographyText.json)} </div> </div> </div> ); } export default Person;
Try the app
Review the app http://localhost:3000/ and click Members links. Also you can add more teams and/ or members to the Team Alpha by adding Content Fragments in AEM.
Under The Hood
Open the browser’s Developer Tools > Network and Filter for all-teams
request. Notice the GraphQL API request /graphql/execute.json/my-project/all-teams
is made against http://localhost:3000
and NOT against the value of REACT_APP_HOST_URI
, for example <https://publish-pxxx-exxx.adobeaemcloud.com
. The requests are made against the React app’s domain because proxy setup is enabled using http-proxy-middleware
module.
Review the main ../setupProxy.js
file and within ../proxy/setupProxy.auth.**.js
files notice how /content
and /graphql
paths are proxied and indicated it’s not a static asset.
module.exports = function(app) {
app.use(
['/content', '/graphql'],
...
Using the local proxy is not a suitable option for production deployment and more details can be found at Production Deployment section.
Congratulations! congratulations
Congratulations! You’ve successfully create the React app to consume and display data from AEM’s GraphQL APIs as part of basic tutorial!