Recreating the Quick Draft dashboard widget using the WordPress Data Layer

Thanks to this great course on Learn WordPress, I’ve recently been inspired by the potential of the WordPress Data Layer. The best way I can describe the data layer is to imagine having a complete UI framework of WordPress components- then combining that with a complete React.js/Redux JavaScript framework hand-tailored to the WordPress REST API. It’s sort of an eye-opening, if not overwhelming, new side of WordPress that is completely untapped.

The introductory course walks you through making an interactive list of your pages, and adds an ability to create/edit page titles. I wanted to see if I could dig into the concepts on another level down, so I decided to recreate the classic “Quick Draft” widget we all know and love on our dashboards. I used a lot of the same code (I’m still no React expert), but synthesizing the parts into something slightly old, slightly new.

Screenshot of the WordPress dashboard. My custom Quick Draft Demo widget sits next to the default Quick Draft widget.
Original on the right, my attempt on the left.

Scaffolding the plugin

I started with the WP-CLI plugin scaffold, just to get a few files in place. I like how one command- wp scaffold plugin --prompt can actually walk you through filling out your entire plugin header. The rest of the scaffold ended up being unnecessary, because it really doesn’t provide anything relating to the world of @wordpress/scripts. I pretty much deleted most of what I had created there, apart from my plugin’s main PHP file, and set up my package.json manually to match the example in the docs. Here’s the important stuff:

  "scripts": {
    "build": "wp-scripts build",
    "format": "wp-scripts format",
    "lint:js": "wp-scripts lint-js",
    "packages-update": "wp-scripts packages-update",
    "start": "wp-scripts start"
  "devDependencies": {
    "@wordpress/scripts": "^24.3.0"
  }Code language: JavaScript (javascript)

I had one issue getting started there- every time I ran npm install, there were no errors, but all the directories inside node_modules were empty. I cleared the npm cache, deleted, tried again, over and over. Nothing helped. I ended up just closing the entire terminal and code editor and re-opening them- somehow that worked. I’m sure there was a reason, but I didn’t see it.

I updated the main plugin file to include the same wp_enqueue_script() method as the demo, which basically uses a PHP array (generated by Webpack) to determine which dependencies I’ll need enqueued. I added an action to register my dashboard widget with trusty old wp_add_dashboard_widget(). The render call back there was just an empty <div> with an ID. That’s when you know you’re getting all JS frameworky- when you’re starting with that empty element!

function add_quick_draft_demo_widget() {
	wp_add_dashboard_widget( 'quick-draft-demo-widget', 'Quick Draft Demo', 'render_quick_draft_demo_widget' );
add_action( 'wp_dashboard_setup', 'add_quick_draft_demo_widget' );

function render_quick_draft_demo_widget() {
	echo '<div id="quick-draft-demo"></div>';
}Code language: PHP (php)

Next I added my src/index.js file and that was literally it for the plugin.

I’ve been thinking a lot about the future of block themes, and how modern theme developers can really miss the simplicity of being able to edit anything by adding to their functions.php file. I’m hopeful that we’ll end up with something like a functions.js file one day that just does all of this stuff automatically – lets me import from these awesome new WordPress packages, handles dependencies, exposes hooks/filters for everything, maybe even handles the build process when WordPress loads. What we have is getting pretty close to that idea. Or maybe that extensibility is what theme.json will end up being, too. Only time will tell.


This is really the coolest piece for me- the @wordpress/components package. I love me some off-the-shelf UI components, I really do. I was able to build the entire form, including the inputs, button, loading spinner indicators, etc all with these WordPress components. They match the user interface (mostly) and bring some much needed consistency to the WordPress admin. I can’t wait until more settings pages are built with these babies.

Here’s what the form looks like:

return (
		<TextControl label="Title" value={ title } onChange={ ( value ) => setTitle( value ) } />

		<TextareaControl label="Content" value={ content } onChange={ ( value ) => setContent( value ) } />

		{ lastError ? (
			<div className="form-error">
				Error: { lastError.message }
		) : false }

		<Button variant="primary" onClick={ handleSave } disabled={ isSaving }>
			{ isSaving ? (
			) : 'Save Draft' }
);Code language: HTML, XML (xml)

With that toolkit ready, I built out the form and a few other custom components in minutes. I was never really a fan of the JSX syntax, but as I’m starting to understand it, there is something nice about writing each piece of the UI as a separate little function with it’s ugly little render () at the end.

At one point, I was thinking I might need to add some custom CSS. See, in the original Quick Draft widget, there’s two text items on the same line, an <h2> element that sits over to the left, and a link that is absolutely positioned on the right. These days, that’s a simple layout with a little flexbox and, well, wouldn’t you know it, there’s a set of <Flex /> components sitting right there in WordPress for me. One import statement later and I’m flexing away.

	<h2>Your Recent Drafts</h2>
	<a href="/wp-admin/edit.php?post_status=draft">View all drafts</a>
</Flex>Code language: HTML, XML (xml)

HTML and Formatting

One of the trickier parts was getting the list of “Your Recent Drafts” to look just right. The post date returned by the REST API is not formatted nicely, and the excerpt includes the “Read More” link and the [...] tag. Of course, WordPress has you covered. There’s a @wordpress/date package that gives you a date function just like the one in PHP. No mucking around with trying to install Moment.js or some other third-party dependency. While the date package might use Moment.js under the hood, I’m not the one worrying about installing it. Again- one import and I’ve got date functionality that I’m already really comfortable with.

Similarly, I needed to do some formatting with the post excerpt and found the @wordpress/autop package ready to go. One of many packages (translation being another good example) that takes established WordPress conventions and turns them into a JavaScript function.

function PostListItem( { post } ) {
	const excerpt = post.excerpt.rendered.split( '[&#8230;]' );
	return (
		<li key={post.id }>
				<a href={ 'post.php?post=' + post.id + '&action=edit' }>
					{ post.title.rendered ? decodeEntities( post.title.rendered ) : '(no title)' }
				<time>{ date('F j, Y', post.date ) }</time>
			{  removep( decodeEntities( excerpt[0] ) ) }
}Code language: JavaScript (javascript)

There’s a few more functions that I haven’t found JavaScript equivalents for just yet- admin_url() and add_query_arg() would’ve come in handy here- but I haven’t spent enough time going through all the packages yet. For WordPress veterans, those custom functions are pure muscle memory at this point, so it’s hard having this adjustment period. I’ll also note that I haven’t dug into security and sanitization either- it can feel a little weird not having my wp_nonce_field() functions front and center.

React, Redux, and State

For me, this is probably the biggest barrier for most old-school WordPress developers – you need a pretty solid understanding of this whole new landscape. These new WordPress tools sit on top of something called @wordpress/element. The Element package is literally “an abstraction layer atop React.” It’s React, but with a slight taste of WordPress.

The real power of React comes by managing “state”- i.e. by keeping all of your components and data up-to-date and synced together so that you don’t have to. (Old-school WordPress devs spent a lot of time throwing stuff in jQuery data attributes and calling things like this: $('.slider').data('active-slide');)

WordPress offers two packages: @wordpress/data for a Redux-like state management and @wordpress/core-data for easy REST API access to stuff in the WordPress database, like users and posts. Here’s how easy it was to fetch our post drafts:

const posts = useSelect(
	select =>
		select( coreDataStore ).getEntityRecords( 'postType', 'post', { status: 'draft', per_page: 3 } ),
);Code language: JavaScript (javascript)

This is where I’m going to be spending my future time- brushing up on React/Redux and getting a firm grasp of the fundamentals. If you’re not super familiar with them- I wouldn’t let that stop you. Novice WordPress developers often use jQuery for a very long time before they finally sit down to understand the fundamentals of JavaScript. I think that’s fine. Novice guitar players may sit down and figure out Stairway to Heaven line by line with almost no understanding of what chords, or even notes, they’re playing. That’s how learning works- you often fake it until you make it.

One last side tangent: My biggest complaint here is the documentation. I know I’m beating a dead horse complaining about Gutenberg’s documentation- and it has come a loooooong way- but the lack of basic considerations in the Block Editor Handbook are probably the biggest barrier to widespread adoption right now. Five to ten-thousand word articles with extremely narrow margins and no navigation or table of contents. It makes them completely unreadable. Contrast that with the PHP documentation which, to this day, continues to innovate and make itself even easier to read and understand. The gap between the PHP code references and the JS handbooks will need to be narrowed significantly.

I’ve uploaded the full (very small!) source code of my project to Github. If you see anything I did wrong, or could’ve done better, please let me know!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.