Building Interfaces with Components

Introduction

WooCommerce uses a combination of PHP and modern JavaScript to create a componentized user interface for merchants that is both performant and extensible. If you have never worked with React or other modern JavaScript tools before, you should take a look at the Getting Started guide for React and work through their Intro to React tutorial to get a better understanding of the concepts involved in building modern user interfaces with React before diving in to this guide.

The examples below are based on code that was generated using the extension generator bundled with WooCommerce Admin. If you’re starting a new extension, the extension generator is the recommended approach. If you’re working with an existing extension, your files may look a bit different.

Whether you’re starting fresh or adding functionality to an existing extension, the core concepts are the same:

  • Use PHP to register and enqueue bundled JavaScript files that contain your components.
  • Use JavaScript to define the structure, state, and behavior of your components.
  • Use actions and filters to provide your component to WooCommerce’s admin interface.

This guide assumes you have a JavaScript build system already set up in your project. Setting up a project to support WooCommerce’s React development workflow is outside the scope of this guide, but if you need guidance for manually adding React support to an existing WooCommerce extension, take a look at this walkthrough on the WooCommerce Developer Blog.


Enqueuing JavaScript and stylesheets

There are different ways to go about enqueing JavaScript and CSS files in WordPress. If you use the WooCommerce Extension Generator, it will scaffold out a registration function for you and hook it to the admin_enqueue_scripts action:

/**
 * Register the JS.
 */
function add_extension_register_script() {

    if ( ! class_exists( 'Automattic\WooCommerce\Admin\Loader' ) || ! \Automattic\WooCommerce\Admin\Loader::is_admin_page() ) {
        return;
    }

    $script_path = '/build/index.js';
    $script_asset_path = dirname( __FILE__ ) . '/build/index.asset.php';
    $script_asset = file_exists( $script_asset_path )
        ? require( $script_asset_path )
        : array( 'dependencies' => array(), 'version' => filemtime( $script_path ) );
    $script_url = plugins_url( $script_path, __FILE__ );

    wp_register_script(
        'my-component-example',
        $script_url,
        $script_asset['dependencies'],
        $script_asset['version'],
        true
    );

    wp_register_style(
        'my-component-example',
        plugins_url( '/build/style.css', __FILE__ ),
        // Add any dependencies styles may have, such as wp-components.
        array(),
        filemtime( dirname( __FILE__ ) . '/build/style.css' )
    );

    wp_enqueue_script( 'my-component-example' );
    wp_enqueue_style( 'my-component-example' );
}

add_action( 'admin_enqueue_scripts', 'add_extension_register_script' );

If you’re using the WooCommerce Extension Generator, your build system will be set up to transpile the JavaScript and SCSS files in your project’s src/ directory into minified static assets in the build/ directory.

Note that the example above also uses the index.asset.php file to provide an array of dependencies when registering this extension’s script. If your build system doesn’t generate this file, you may need to specify these dependencies manually to ensure your extension functions correctly.


Building components that display static data

When using the WooCommerce Extension Generator, your extension will be set up to have you create your components and styles in the src/ directory.

The typical entry-point for processing your extension’s components will be the src/index.js file. Think of this file as containing the top-level of your extension’s component hierarchy.

It’s a good idea to use this file for importing top-level components that your extension relies on and for injecting those components into the admin experience using hooks.

As simple example, we’ll start with the custom page we created in the previous section of this guide and populate that page with a nested component that adds a widget containing a list of recent product reviews.

Let’s start by importing a few dependencies to our main JavaScript file (src/index.js).

/**
 * External dependencies
 */

import { addFilter } from '@wordpress/hooks';

/**
 * WooCommerce dependencies
 */
import {
    H,
    Section,
    Table,
} from '@woocommerce/components';

/**
 * Internal dependencies
 */
// import MyInternalComponent from './my-internal-component';

Tip: Handling Imports

In all of your JavaScript files, it’s best to keep your import statements organized into external dependencies, WooCommerce dependencies, and internal modules from within your project itself. This helps make your code easier to maintain and debug when issues arise.

Setting up the component hierarchy

As mentioned above, we’re starting with the custom page we registered in the previous section.

const MyExamplePage = () => <h1>My Example Extension</h1>;

addFilter( 'woocommerce_admin_pages_list', 'my-namespace', ( pages ) => {
    pages.push( {
        container: MyExamplePage,
        path: '/example',
        breadcrumbs: [ 'My Example Page' ],
        navArgs: {
            id: 'my-example-page',
        },
    } );

    return pages;
} );

For this example, we’ll swap out the existing MyExamplePage component with one that represents our custom component hierarchy. Let’s start by expanding upon MyExamplePage and replacing the H1 element with a bit of static data and a return statement that uses the data to populate a slightly more complex JSX template.

const MyExamplePage = () => {
    // Using static data for demonstration.
    const recentReviewData = [
        {
            id: 33,
            product_id: 5,
            rating: 4,
            review: 'Great quality'
        },
        {
            id: 32,
            product_id: 17,
            rating: 2, 
            review: 'Runs small.'
        },
        {
            id: 31,
            product_id: 42,
            rating: 5, 
            review: 'Fantastic!.'
        }
    ];

    const headers = [
        { label: 'Product ID' },
        { label: 'Rating' },
        { label: 'Comments' },
    ]

    const rowData = [
        recentReviewData.map( ( review )) => {
            return [
                { display: review.product_id, value: review.product_id },
                { display: review.rating, value: review.rating },
                { display: review.review, value: false }
            ]
        }
    ];

    return (
        <>
            <H>My Example Extension</H>;
            <Section component='article'>
                <p>This is a table of recent reviews</p>
                <Table
                    caption= "Recent Reviews"
                    rows = { rowData }
                    headers = { headers }
                />
            </Section>
        </>
    )
}

Note: The component used in this example is a function component. React also supports class-based components, but function components are quickly becoming the prevailing pattern in the WordPress/Gutenberg ecosystem.

The changes we made above assign some values to constants which we use to populate the properties of the Table component in our component’s return statement. You can read more about available properties and their expected format in the Table component documentation.

Tip: Setting up a data placeholder

Before pulling live data, it can be helpful use some static data to get your component’s basic hierarchy and behavior set up. This is what we’ve done with recentReviewData above. Once you have everything working, you can swap out your static data with functionality that will fetch data from the WooCommerce REST API.

If you rebuild your extension’s JavaScript files and browse to your custom page, you should the content has been updated with the new components we created.


Querying live data to display in components

Once we know our component is displaying everything correctly, we can swap out our static data with functionality that queries the WooCommerce REST API for data when our component loads. There are several different ways to do that, including a collection of data stores that WooCommerce Admin uses to abstract API calls. For this example, however, let’s build a simple asyncronous function that will exist alongside our MyExamplePage component’s function.

Since we’re going to be working with asynchronous network calls and mutable data, we’ll start by importing a few utility functions to help us manage the state and lifecycle of our component using React Hooks. Let’s update our component’s external dependencies to import React’s useState(), useEffect() and useRef() hooks, which are all available via the @wordpress/element package.

/**
 * External dependencies
 */
import { useEffect, useRef, useState } from '@wordpress/element';

Now let’s update our component to make use of some of these functions.

const MyExamplePage = () => {
    const [recentReviewData, setRecentReviewData] = useState([]);

    const headers = [
        { label: 'Product ID' },
        { label: 'Rating' },
        { label: 'Comments' },
    ]

    const rowData = recentReviewData.map( ( review ) => {
    return [
            { display: review.product_id, value: review.product_id },
            { display: review.rating, value: review.rating },
            { display: review.review, value: false }
        ]
  });

    useEffect(() => {
        fetchRecentReviewData(setRecentReviewData);
    }, []);

    return (
        <>
            <H>My Example Extension</H>
            <Section component='article'>
                <p>This is a table of recent reviews</p>
                <Table
                    caption= "Recent Reviews"
                    rows = { rowData }
                    headers = { headers }
                />
            </Section>
        </>
    )
}

The useState function lets us declare our recentReviewData constant, populate it with an initial value, and also define a function that we can call to update the value.

The useEffect function takes a callback function as its first argument and an optional second argument that tells React which property changes should trigger the callback function. We’re passing an empty array as the second argument, which tells React to execute the callback function only once: on the initial render. Were we to omit the second argument, our callback function would run every time our component renders, which, in a real-world scenario, could result in lots of superfluous network requests that would affect a store’s performance. You can read more about the useEffect function’s behavior in the Hooks section of the React documentation.

The callback function we have passed as our first argument to useEffect includes a call to a function called fetchRecentReviewData and passes the state setter function we defined earlier with the useState hook. Let’s define that function now:

const fetchRecentReviewData = async ( setRecentReviewData = () => void null ) => {
    const reviewsUrl = '/wc/v3/products/reviews'

    await wp.apiFetch({
        path: reviewsUrl
    }).then( (data) => {
            setRecentReviewData( data );
        });
}

This will run our REST API request when the component renders and will use the returned data to update the state of our component.

What happens if the component re-renders before the network request is complete?

If the component re-renders or unmounts before the network request is complete, we’re likely to see a memory leak warning from React as it attempts to modify the state of an unmounted component. We can put a safeguard in place to handle this situation with the help of that useRef() hook we imported earlier.

First, we’ll create a Ref inside our function component, which helps us keep track of whether our MyExamplePage component is mounted or not.

const mounted = useRef( true );

Then we’ll modify what we’re passing to useEffect() to add a bit of cleanup and conditional logic to make sure that our effect only runs if the component is mounted.

useEffect( () => {
  const updateState = ( recentReviewData ) => {
    if ( mounted.current ) {
      setRecentReviewData( recentReviewData );
    }
  }
  fetchRecentReviewData( updateState );

  return () => {
      mounted.current = false
  }
}, [] );

This safeguard ensures that React will only try to update the state of the component if it is mounted.

Putting it all together

Here’s what our JavaScript file should look like with everything in place:

/**
 * External dependencies
 */
import { addFilter } from '@wordpress/hooks';
import { useEffect, useRef, useState } from '@wordpress/element';

/**
 * WooCommerce dependencies
 */
import {
    H,
    Section,
    Table,
} from '@woocommerce/components';


/**
 * MyExamplePage Component
 */
const MyExamplePage = () => {
    const [recentReviewData, setRecentReviewData] = useState([]);
    const mounted = useRef( true );

    const headers = [
        { label: 'Product ID' },
        { label: 'Rating' },
        { label: 'Comments' },
    ]

    const rowData = recentReviewData.map( ( review ) => {
        return [
            { display: review.product_id, value: review.product_id },
            { display: review.rating, value: review.rating },
            { display: review.review, value: false }
        ]
    });

    const fetchRecentReviewData = async ( setRecentReviewData = () => void null ) => {
        const reviewsUrl = '/wc/v3/products/reviews'

        await wp.apiFetch({
            path: reviewsUrl
        }).then( (data) => {
            setRecentReviewData( data );
        });
    }

    useEffect( () => {
          const updateState = ( recentReviewData ) => {
        if ( mounted.current ) {
          setRecentReviewData( recentReviewData );
        }
      }
      fetchRecentReviewData( updateState );

      return () => {
        mounted.current = false
      }
    }, [] );

    return (
        <>
            <H>My Example Extension</H>
            <Section component='article'>
                <p>This is a table of recent reviews</p>
                <Table
                    caption= "Recent Reviews"
                    rows = { rowData }
                    headers = { headers }
                />
            </Section>
        </>
    )
}

/**
 * Filter for adding our page to the list of WooCommerce Admin pages
 */ 
addFilter( 'woocommerce_admin_pages_list', 'my-namespace', ( pages ) => {
    pages.push( {
        container: MyExamplePage,
        path: '/example',
        breadcrumbs: [ 'My Example Page' ],
        navArgs: {
            id: 'my-example-page',
        },
    } );

    return pages;
} );

Note: As mentioned above, we’re using React hooks in this example, which allow us to take advantage of React features such as lifecycle functions without needing to write an entire class. In addition to the hooks that are built in with React, there are also WordPress-specific hooks that you can take advantage of too. These hooks are especially helpful when working with data stores in WordPress and WooCommerce.


Handling events in components

We can handle events in our components similar to how we handled the REST API request. We simply create a method to handle the event and then attach that method to the event handler for the appropriate element in our component. As a simple example, if we wanted to add a click event handler for the H element on our top-level component, we’d need to do a few things:

  1. Create the event handler function inside our component’s function
  2. Assign the function to the event handler on the element

Create the event handler function

const handleHeadingClick = (e) => {
    e.preventDefault();
    alert('You clicked the header!');
}

Assign the function to the event handler

return (
    <>
        <H onClick={handleHeadingClick}>My Example Extension</H>;
        <Section component='article'>
            <p>This is a table of recent reviews</p>
            <Table
                caption= "Recent Reviews"
                rows = { rowData }
                headers = { headers }
            />
        </Section>
    </>
)

Additional Examples

The examples in this guide are only meant for demonstration. If you’d like to see more examples, take a look at the sample extensions available in the WooCommerce Admin repository. You can also read more about components in the WooCommerce Admin developer documentation and the WordPress Components reference.