I saw a twitter discussion recently that got me thinking.
When building plugins with custom admin pages, one of the more modern approaches is to build them in React, and re-use the Gutenberg component library for building out the UI so we get a nice native-feeling experience.
I’m all for this and doing it in all the plugins I’m working on at the moment, the upcoming version of Search & Filter heavily leverages this.
The discussion then turned to, “how to pass data from PHP to React” – I’ll be sharing my approach and answer to that question below.
Whats wrong with wp_localize_script()
?
Well, nothing at all. It’s a great way to attach JavaScript variables to your script or application and it’s been used forever across WordPress, and it’s relatively straight forward to use.
90% of the tutorials out there will show you this technique and I have nothing against it.
The only limitation is that it is loaded on page load, before your JavaScript is run, so the data is not interactive in anyway.
This leads me on to what I think is a better approach when building admin pages using React.
React and the @wordpress/data
package
I’m assuming in this post you are already using React for your custom admin pages (or have started to), and you’re familiar with the REST API as a way to fetch data.
Now that’s out of the way, its worth mentioning (quite briefly) that one of the best ways to interact with the REST API in a WordPress based React application is via the @wordpress/data
package.
Working with custom stores for your own REST API endpoints is complex in its own right and I’m not going to be able to explain it here quickly or succinctly, I’ll leave that to people before me who have already done it well.
One of my favorite and more technical introductions on the @wordpress/data
package is this series from Darren Ethier (a la WooCommerce) – it’s a little out dated but it does go in quite deep and most of it still works correctly – it has certainly helped me out!
The official documentation has also improved and gives a good working example via their shop example at the above link.
If anyone has any better resources I’d be happy to include them here, just let me know.
To summarise my thoughts on this: the WordPress @wordpress/data
package is the official WordPress supported way of interacting with data and the REST API in your React applications, and while it’s quite complex and has a learning curve, the rewards are well worth it in my opinion.
But there is one caveat when using this on custom admin pages, but first I’ll get to why we shouldn’t use it…
When shouldn’t we use @wordpress/data
I think the data package and it’s learning curve can be complete overkill for some use cases.
If your admin screens are not very interactive, or don’t need to use Ajax or the REST API, then I would recommend preloading your data and pass it into your app as a JavaScript object via wp_localize_script()
.
However, if you need interactivity, are using data that needs to be saved and updated, or doing anything with the classic WordPress Ajax hooks (wp_ajax_...
) then I would recommend to start using the newer data package.
Back to the caveat: what if we want data on page load
Unlike wp_localize_script()
, using the data package doesn’t inject data into your application on page load.
- First your JavaScript application needs to initialize
- Then a request is made the a REST API endpoint
- Then we need to wait for the response to get our data
No one likes looking at loading icons after loading a page, its frustrating for everyone.
A possible solution could be:
- Use
wp_localize_script()
for the initial data - Use
@wordpress/data
for the rest
I’m not a fan of this approach, I’ve tried it and have some thoughts:
- What happens if the data you fetch via the REST API collides with the data you preloaded? Work arounds could be made but things can get messy quickly.
- Why does an application need multiple methods of handling data? One streamlined API for everything would be ideal.
Surely there has got to be a better approach?!
Preloading our REST API endpoints
If we could preload specific REST API endpoints on page load, then we wouldn’t need to have multiple methods of injecting data in our application.
Fortunately, taking a browse through WordPress core gives us all the information we need to achieve this today.
The block editor is already using the preloading REST API endpoints technique extensively, I first discovered this via the hook block_editor_rest_api_preload
which allows you to add your own endpoints for preloading requests in the block editor.
Without this we’d be staring at loading icons forever whenever we want to edit a post or page.
The secret sauce for how this all works is in the WordPress core function block_editor_rest_api_preload()
found in wp-includes\block-editor.php
While it’s probably entirely possible to re-use this function “as is” on your own admin screen there are a couple of issues I have with it:
- If you use this function on your admin screens, other plugins hooking into
block_editor_rest_api_preload
will load their assets and endpoints, probably not what you need or expected. - The second argument to the function is
$block_editor_context
, and while I don’t understand it fully, it references things like$block_editor_context->post
, and I’m not dealing with posts on my admin screens, so I think this is just not meant for use in custom admin screens.
Creating a new preload function for custom admin screens
Taking the above code from core, and stripping out everything that does not seem relevant for my use case, I’ve come up with this function:
/**
* Preload the API requests.
*
* @param array $preload_paths The paths to preload.
*/
function rmcodes_preload_api_requests( $preload_paths ) {
/* Copied from core and modified - wp-includes/block-editor.php */
// Restore the global $post as it was before API preloading.
// Preload common data.
global $post, $wp_scripts, $wp_styles, $post_id;
/*
* Ensure the global $post, $wp_scripts, and $wp_styles remain the same after
* API data is preloaded.
* Because API preloading can call the_content and other filters, plugins
* can unexpectedly modify the global $post or enqueue assets which are not
* intended for the block editor.
*/
$backup_global_post = ! empty( $post ) ? clone $post : $post;
$backup_wp_scripts = ! empty( $wp_scripts ) ? clone $wp_scripts : $wp_scripts;
$backup_wp_styles = ! empty( $wp_styles ) ? clone $wp_styles : $wp_styles;
$backup_post_id = $post_id;
$preload_data = array_reduce(
$preload_paths,
'rest_preload_api_request',
array()
);
//phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$post = $backup_global_post;
//phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$wp_scripts = $backup_wp_scripts;
//phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$wp_styles = $backup_wp_styles;
//phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$post_id = $backup_post_id;
wp_add_inline_script(
'wp-api-fetch',
sprintf(
'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
wp_json_encode( $preload_data )
),
'after'
);
wp_add_inline_script(
'wp-api-fetch',
'window.apiResult = ' . wp_json_encode( $preload_data ),
'after'
);
}
And I use this in the admin_enqueue_scripts
hook:
/**
* Preload REST API endpoint.
*
* @param string $hook Hook suffix for the current admin page.
*/
function rmcodes_enqueue_admin_script() {
// Don't load it on every admin screen (!), check and return early...
// The REST API paths to preload.
$preload_paths = array(
'/rm-codes/v1/admin/data',
'/rm-codes/v1/admin/pages',
'/rm-codes/v1/another-endpoint',
);
rmcodes_preload_api_requests( $preload_paths );
}
add_action( 'admin_enqueue_scripts', 'rmcodes_enqueue_admin_script' );
And that’s it! Now you can preload your own REST API endpoints on your admin screens, streamlining the flow of your data when using the @wordpress/data
package.
You can use apiFetch
without the data package
That’s right, you don’t actually need to use @wordpress/data
at all…
This approach short circuits the WordPress apiFetch()
javascript function, which means any type of data fetching via the REST API with WordPress’ apiFetch
should be compatible.
It is still recommened to use a store of some kind rather than running apiFetch
in your components, data fetching and management shouldn’t really be done in a components useEffect
hook.
Further thoughts
It would be great if WordPress could provide a base function like this, a possible name could be wp_rest_api_preload()
which passes in the current admin screen as an argument, for developers to tap into. This function could then be reused for things like the block editor and the block_editor_rest_api_preload()
function.
It’s clearly becoming more and more common for developers to use React for custom admin screens and giving a nice integrated way to do this would be a big help.
If you have any alternative approaches or thoughts I’m keen to hear them!