This is a continuation of my last article about “Rendering External API Data in WordPress Blocks on the Front End”. In that last one, we learned how to take an external API and integrate it with a block that renders the fetched data on the front end of a WordPress site.
The thing is, we accomplished this in a way that prevents us from seeing the data in the WordPress Block Editor. In other words, we can insert the block on a page but we get no preview of it. We only get to see the block when it’s published.
Let’s revisit the example block plugin we made in the last article. Only this time, we’re going to make use of the JavaScript and React ecosystem of WordPress to fetch and render that data in the back-end Block Editor as well.
Working With External APIs in WordPress Blocks
- Rendering Data on the Front End
- Rendering Data on the Back End (you are here!)
- Creating a Custom Settings UI
- Saving Custom Block Settings
- Working With Live API Data (coming soon)
Where we left off
As we kick this off, here’s a demo where we landed in the last article that you can reference. You may have noticed that I used a render_callback
method in the last article so that I can make use of the attributes in the PHP file and render the content.
Well, that may be useful in situations where you might have to use some native WordPress or PHP function to create dynamic blocks. But if you want to make use of just the JavaScript and React (JSX, specifically) ecosystem of WordPress to render the static HTML along with the attributes stored in the database, you only need to focus on the Edit
and Save
functions of the block plugin.
- The
Edit
function renders the content based on what you want to see in the Block Editor. You can have interactive React components here. - The
Save
function renders the content based on what you want to see on the front end. You cannot have the the regular React components or the hooks here. It is used to return the static HTML that is saved into your database along with the attributes.
The Save
function is where we’re hanging out today. We can create interactive components on the front-end, but for that we need to manually include and access them outside the Save
function in a file like we did in the last article.
So, I am going to cover the same ground we did in the last article, but this time you can see the preview in the Block Editor before you publish it to the front end.
The block props
I intentionally left out any explanations about the edit
function’s props in the last article because that would have taken the focus off of the main point, the rendering.
If you are coming from a React background, you will likely understand what is that I am talking about, but if you are new to this, I would recommend checking out components and props in the React documentation.
If we log the props
object to the console, it returns a list of WordPress functions and variables related to our block:
We only need the attributes
object and the setAttributes
function which I am going to destructure from the props
object in my code. In the last article, I had modified RapidAPI’s code so that I can store the API data through setAttributes()
. Props are only readable, so we are unable to modify them directly.
Block props are similar to state variables and setState
in React, but React works on the client side and setAttributes()
is used to store the attributes permanently in the WordPress database after saving the post. So, what we need to do is save them to attributes.data
and then call that as the initial value for the useState()
variable.
edit
function
The I am going to copy-paste the HTML code that we used in football-rankings.php
in the last article and edit it a little to shift to the JavaScript background. Remember how we created two additional files in the last article for the front end styling and scripts? With the way we’re approaching things today, there’s no need to create those files. Instead, we can move all of it to the Edit
function.
Full code
import { useState } from "@wordpress/element";
export default function Edit(props) {
const { attributes, setAttributes } = props;
const [apiData, setApiData] = useState(null);
function fetchData() {
const options = {
method: "GET",
headers: {
"X-RapidAPI-Key": "Your Rapid API key",
"X-RapidAPI-Host": "api-football-v1.p.rapidapi.com",
},
};
fetch(
"https://api-football-v1.p.rapidapi.com/v3/standings?season=2021&league=39",
options
)
.then((response) => response.json())
.then((response) => {
let newData = { ...response }; // Deep clone the response data
setAttributes({ data: newData }); // Store the data in WordPress attributes
setApiData(newData); // Modify the state with the new data
})
.catch((err) => console.error(err));
}
return (
<div {...useBlockProps()}>
<button onClick={() => getData()}>Fetch data</button>
{apiData && (
<>
<div id="league-standings">
<div
className="header"
style={{
backgroundImage: `url(${apiData.response[0].league.logo})`,
}}
>
<div className="position">Rank</div>
<div className="team-logo">Logo</div>
<div className="team-name">Team name</div>
<div className="stats">
<div className="games-played">GP</div>
<div className="games-won">GW</div>
<div className="games-drawn">GD</div>
<div className="games-lost">GL</div>
<div className="goals-for">GF</div>
<div className="goals-against">GA</div>
<div className="points">Pts</div>
</div>
<div className="form-history">Form history</div>
</div>
<div className="league-table">
{/* Usage of [0] might be weird but that is how the API structure is. */}
{apiData.response[0].league.standings[0].map((el) => {
{/* Destructure the required data from all */}
const { played, win, draw, lose, goals } = el.all;
return (
<>
<div className="team">
<div class="position">{el.rank}</div>
<div className="team-logo">
<img src={el.team.logo} />
</div>
<div className="team-name">{el.team.name}</div>
<div className="stats">
<div className="games-played">{played}</div>
<div className="games-won">{win}</div>
<div className="games-drawn">{draw}</div>
<div className="games-lost">{lose}</div>
<div className="goals-for">{goals.for}</div>
<div className="goals-against">{goals.against}</div>
<div className="points">{el.points}</div>
</div>
<div className="form-history">
{el.form.split("").map((result) => {
return (
<div className={`result-${result}`}>{result}</div>
);
})}
</div>
</div>
</>
);
}
)}
</div>
</div>
</>
)}
</div>
);
}
I have included the React hook useState()
from @wordpress/element
rather than using it from the React library. That is because if I were to load the regular way, it would download React for every block that I am using. But if I am using @wordpress/element
it loads from a single source, i.e., the WordPress layer on top of React.
This time, I have also not wrapped the code inside useEffect()
but inside a function that is called only when clicking on a button so that we have a live preview of the fetched data. I have used a state variable called apiData
to render the league table conditionally. So, once the button is clicked and the data is fetched, I am setting apiData
to the new data inside the fetchData()
and there is a rerender with the HTML of the football rankings table available.
You will notice that once the post is saved and the page is refreshed, the league table is gone. That is because we are using an empty state (null
) for apiData
‘s initial value. When the post saves, the attributes are saved to the attributes.data
object and we call it as the initial value for the useState()
variable like this:
const [apiData, setApiData] = useState(attributes.data);
save
function
The We are going to do almost the same exact thing with the save
function, but modify it a little bit. For example, there’s no need for the “Fetch data” button on the front end, and the apiData
state variable is also unnecessary because we are already checking it in the edit
function. But we do need a random apiData
variable that checks for attributes.data
to conditionally render the JSX or else it will throw undefined errors and the Block Editor UI will go blank.
Full code
export default function save(props) {
const { attributes, setAttributes } = props;
let apiData = attributes.data;
return (
<>
{/* Only render if apiData is available */}
{apiData && (
<div {...useBlockProps.save()}>
<div id="league-standings">
<div
className="header"
style={{
backgroundImage: `url(${apiData.response[0].league.logo})`,
}}
>
<div className="position">Rank</div>
<div className="team-logo">Logo</div>
<div className="team-name">Team name</div>
<div className="stats">
<div className="games-played">GP</div>
<div className="games-won">GW</div>
<div className="games-drawn">GD</div>
<div className="games-lost">GL</div>
<div className="goals-for">GF</div>
<div className="goals-against">GA</div>
<div className="points">Pts</div>
</div>
<div className="form-history">Form history</div>
</div>
<div className="league-table">
{/* Usage of [0] might be weird but that is how the API structure is. */}
{apiData.response[0].league.standings[0].map((el) => {
const { played, win, draw, lose, goals } = el.all;
return (
<>
<div className="team">
<div className="position">{el.rank}</div>
<div className="team-logo">
<img src={el.team.logo} />
</div>
<div className="team-name">{el.team.name}</div>
<div className="stats">
<div className="games-played">{played}</div>
<div className="games-won">{win}</div>
<div className="games-drawn">{draw}</div>
<div className="games-lost">{lose}</div>
<div className="goals-for">{goals.for}</div>
<div className="goals-against">{goals.against}</div>
<div className="points">{el.points}</div>
</div>
<div className="form-history">
{el.form.split("").map((result) => {
return (
<div className={`result-${result}`}>{result}</div>
);
})}
</div>
</div>
</>
);
})}
</div>
</div>
</div>
)}
</>
);
}
If you are modifying the save
function after a block is already present in the Block Editor, it would show an error like this:
That is because the markup in the saved content is different from the markup in our new save
function. Since we are in development mode, it is easier to remove the bock from the current page and re-insert it as a new block — that way, the updated code is used instead and things are back in sync.
This situation of removing it and adding it again can be avoided if we had used the render_callback
method since the output is dynamic and controlled by PHP instead of the save function. So each method has it’s own advantages and disadvantages.
Tom Nowell provides a thorough explanation on what not to do in a save
function in this Stack Overflow answer.
Styling the block in the editor and the front end
Regarding the styling, it is going to be almost the same thing we looked at in the last article, but with some minor changes which I have explained in the comments. I’m merely providing the full styles here since this is only a proof of concept rather than something you want to copy-paste (unless you really do need a block for showing football rankings styled just like this). And note that I’m still using SCSS that compiles to CSS on build.
Editor styles
/* Target all the blocks with the data-title="Football Rankings" */
.block-editor-block-list__layout
.block-editor-block-list__block.wp-block[data-title="Football Rankings"] {
/* By default, the blocks are constrained within 650px max-width plus other design specific code */
max-width: unset;
background: linear-gradient(to right, #8f94fb, #4e54c8);
display: grid;
place-items: center;
padding: 60px 0;
/* Button CSS - From: https://getcssscan.com/css-buttons-examples - Some properties really not needed :) */
button.fetch-data {
align-items: center;
background-color: #ffffff;
border: 1px solid rgb(0 0 0 / 0.1);
border-radius: 0.25rem;
box-shadow: rgb(0 0 0 / 0.02) 0 1px 3px 0;
box-sizing: border-box;
color: rgb(0 0 0 / 0.85);
cursor: pointer;
display: inline-flex;
font-family: system-ui, -apple-system, system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 16px;
font-weight: 600;
justify-content: center;
line-height: 1.25;
margin: 0;
min-height: 3rem;
padding: calc(0.875rem - 1px) calc(1.5rem - 1px);
position: relative;
text-decoration: none;
transition: all 250ms;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
vertical-align: baseline;
width: auto;
&:hover,
&:focus {
border-color: rgb(0, 0, 0, 0.15);
box-shadow: rgb(0 0 0 / 0.1) 0 4px 12px;
color: rgb(0, 0, 0, 0.65);
}
&:hover {
transform: translateY(-1px);
}
&:active {
background-color: #f0f0f1;
border-color: rgb(0 0 0 / 0.15);
box-shadow: rgb(0 0 0 / 0.06) 0 2px 4px;
color: rgb(0 0 0 / 0.65);
transform: translateY(0);
}
}
}
Front-end styles
/* Front-end block styles */
.wp-block-post-content .wp-block-football-rankings-league-table {
background: linear-gradient(to right, #8f94fb, #4e54c8);
max-width: unset;
display: grid;
place-items: center;
}
#league-standings {
width: 900px;
margin: 60px 0;
max-width: unset;
font-size: 16px;
.header {
display: grid;
gap: 1em;
padding: 10px;
grid-template-columns: 1fr 1fr 3fr 4fr 3fr;
align-items: center;
color: white;
font-size: 16px;
font-weight: 600;
background-color: transparent;
background-repeat: no-repeat;
background-size: contain;
background-position: right;
.stats {
display: flex;
gap: 15px;
& > div {
width: 30px;
}
}
}
}
.league-table {
background: white;
box-shadow:
rgba(50, 50, 93, 0.25) 0px 2px 5px -1px,
rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
padding: 1em;
.position {
width: 20px;
}
.team {
display: grid;
gap: 1em;
padding: 10px 0;
grid-template-columns: 1fr 1fr 3fr 4fr 3fr;
align-items: center;
}
.team:not(:last-child) {
border-bottom: 1px solid lightgray;
}
.team-logo img {
width: 30px;
top: 3px;
position: relative;
}
.stats {
display: flex;
gap: 15px;
& > div {
width: 30px;
text-align: center;
}
}
.last-5-games {
display: flex;
gap: 5px;
& > div {
width: 25px;
height: 25px;
text-align: center;
border-radius: 3px;
font-size: 15px;
& .result-W {
background: #347d39;
color: white;
}
& .result-D {
background: gray;
color: white;
}
& .result-L {
background: lightcoral;
color: white;
}
}
}
We add this to src/style.scss
which takes care of the styling in both the editor and the frontend. I will not be able to share the demo URL since it would require editor access but I have a video recorded for you to see the demo:
Pretty neat, right? Now we have a fully functioning block that not only renders on the front end, but also fetches API data and renders right there in the Block Editor — with a refresh button to boot!
But if we want to take full advantage of the WordPress Block Editor, we ought to consider mapping some of the block’s UI elements to block controls for things like setting color, typography, and spacing. That’s a nice next step in the block development learning journey.