Build a React app that use AEM鈥檚 GraphQL APIs
In this chapter, you explore how AEM鈥檚 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鈥檚 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
The following software must be installed:
Objectives
Learn how to:
- Download and start the example React app
- Query AEM鈥檚 GraphQL end-points using the
- Query AEM for a list of teams, and their referenced members
- Query AEM for a team member鈥檚 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鈥檚 GraphQL API, and display team and person data obtained from them.
The sample React app source code is available on Github.com at
To get the React app:
-
Clone the sample WKND GraphQL React app from .
code language-shell $ cd ~/Code $ git clone git@github.com:adobe/aem-guides-wknd-graphql.git -
Navigate to
basic-tutorialfolder and open it in your IDE.code language-shell $ cd ~/Code/aem-guides-wknd-graphql/basic-tutorial $ code .
-
Update
.env.developmentto connect to AEM as a Cloud Service Publish service.- Set
REACT_APP_HOST_URI鈥檚 value to be your AEM as a Cloud Service鈥檚 Publish URL (ex.REACT_APP_HOST_URI=https://publish-p123-e456.adobeaemcloud.com) andREACT_APP_AUTH_METHOD鈥檚 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:4502andREACT_APP_AUTH_METHOD鈥檚 value tobasic. - Set
-
From the command line, go to the
aem-guides-wknd-graphql/basic-tutorialfolder -
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 . 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/apifolder contains files used to make GraphQL queries to AEM.src/api/aemHeadlessClient.jsinitializes and exports the AEM Headless Client used to communicate with AEMsrc/api/usePersistedQueries.jsimplements return data from AEM GraphQL to theTeams.jsandPerson.jsview components.
-
The
src/components/Teams.jsfile displays a list of teams and their members, by using a list query. -
The
src/components/Person.jsfile 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
AEMHeadlessdeclaration from the , 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
serviceUrlsetup for the included configuration, line 27.
-
-
Lines 42-49, are most important, as they instantiate the
AEMHeadlessclient 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 辞产箩别肠迟鈥檚 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.jsupdatefetchPersistedQuery(..), 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鈥檚 main view. This functionality requires:
- A new in
src/api/usePersistedQueries.jsthat invokes themy-project/all-teamspersisted query, returning a list of Team Content Fragments in AEM. - A React component at
src/components/Teams.jsthat invokes the new custom ReactuseEffecthook, and renders the teams data.
Once complete, the app鈥檚 main view populates with the teams data from AEM.
Steps
-
Open
src/api/usePersistedQueries.js. -
Locate the function
useAllTeams() -
To create a
useEffecthook that invokes the persisted querymy-project/all-teamsviafetchPersistedQuery(..), 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
TeamsReact 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
TeamReact 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"></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鈥檚 implement the functionality to handle the display on a team member鈥檚, or person鈥檚, details.
This functionality requires:
-
A new in
src/api/usePersistedQueries.jsthat invokes the parameterizedmy-project/person-by-namepersisted query, and returns a single person record. -
A React component at
src/components/Person.jsthat uses a person鈥檚 full name as a query parameter, invokes the new custom ReactuseEffecthook, and renders the person data.
Once complete, selecting a person鈥檚 name in the Teams view, renders the person view.
-
Open
src/api/usePersistedQueries.js. -
Locate the function
usePersonByName(fullName) -
To create a
useEffecthook that invokes the persisted querymy-project/all-teamsviafetchPersistedQuery(..), 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
PersonReact component, parse thefullNameroute 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 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鈥檚 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鈥檚 domain because 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鈥檚 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鈥檝e successfully create the React app to consume and display data from AEM鈥檚 GraphQL APIs as part of basic tutorial!