Step-by-Step Guide to building a custom CSV bulk exporter block for Gutenberg using SolidJS high-performance reactive components
Leveraging SolidJS for High-Performance Gutenberg CSV Export
For e-commerce platforms, the ability to efficiently export large datasets in CSV format is paramount for analytics, reporting, and third-party integrations. Traditional WordPress approaches often involve server-side PHP loops that can become bottlenecks, especially with substantial data volumes. This guide details building a custom Gutenberg block for CSV export, powered by SolidJS, to deliver a high-performance, client-side reactive experience.
Project Setup and Dependencies
We’ll be using Node.js and npm for managing our project. The core dependencies include SolidJS for the UI components and a build tool like Vite for efficient compilation and development server. Ensure you have Node.js installed. If not, download it from nodejs.org.
First, let’s set up a new WordPress plugin directory and initialize our project with npm:
- Create a new plugin folder:
mkdir my-csv-exporter - Navigate into the folder:
cd my-csv-exporter - Initialize npm:
npm init -y - Install necessary development dependencies:
npm install --save-dev vite @vitejs/plugin-react solid-refresh @babel/core @babel/preset-env @babel/preset-react babel-loader css-loader mini-css-extract-plugin sass sass-loader webpack webpack-cli
While Vite is generally preferred for its speed, for WordPress plugin development, integrating with the existing build process or using a Webpack configuration tailored for WordPress plugins is often more practical. For this example, we’ll outline a conceptual Vite setup, which can be adapted to Webpack or other bundlers. A typical vite.config.js might look like this:
Vite Configuration (Conceptual)
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';
import path from 'path';
export default defineConfig({
plugins: [solidPlugin()],
build: {
outDir: 'build', // Output directory for compiled assets
emptyOutDir: true,
rollupOptions: {
input: {
editor: path.resolve(__dirname, 'src/editor.js'), // Entry point for Gutenberg editor
script: path.resolve(__dirname, 'src/script.js'), // Entry point for frontend script
},
output: {
entryFileNames: '[name].js',
chunkFileNames: 'chunks/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
},
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
Gutenberg Block Registration
We need to register our custom block with WordPress. This involves creating a PHP file (e.g., my-csv-exporter.php) in the plugin’s root directory to enqueue our compiled JavaScript and define the block’s attributes.
<?php
/**
* Plugin Name: My CSV Exporter
* Description: A custom Gutenberg block for high-performance CSV export.
* Version: 1.0.0
* Author: Your Name
*/
function my_csv_exporter_register_block() {
// Automatically load dependencies and version
$asset_file = include( plugin_dir_path( __FILE__ ) . 'build/index.asset.php'); // Assuming build/index.asset.php is generated by Vite/Webpack
wp_register_script(
'my-csv-exporter-block-editor-script',
plugins_url( 'build/editor.js', __FILE__ ),
$asset_file['dependencies'],
$asset_file['version']
);
wp_register_script(
'my-csv-exporter-block-frontend-script',
plugins_url( 'build/script.js', __FILE__ ),
array(), // No specific dependencies for frontend script if logic is client-side
$asset_file['version']
);
register_block_type( 'my-csv-exporter/csv-exporter', array(
'editor_script' => 'my-csv-exporter-block-editor-script',
'script' => 'my-csv-exporter-block-frontend-script',
'attributes' => array(
'data' => array(
'type' => 'string',
'default' => '',
),
'fileName' => array(
'type' => 'string',
'default' => 'export.csv',
),
),
) );
}
add_action( 'init', 'my_csv_exporter_register_block' );
?>
The index.asset.php file is crucial for WordPress to correctly manage script dependencies and versions. If you’re using Vite, you’ll need a plugin or a custom script to generate this file based on your package.json and Vite’s output. For Webpack, this is often handled by the @wordpress/scripts package.
SolidJS Component for the Gutenberg Editor
This is where the high-performance aspect comes into play. We’ll create a SolidJS component that handles the data fetching (or selection) and the CSV generation logic client-side. This component will be rendered within the Gutenberg editor.
src/components/CsvExporter.jsx
import { createSignal, For } from 'solid-js';
import { createStore } from 'solid-js/store';
// Helper function to convert JSON data to CSV string
function convertToCSV(data, headers) {
const rows = data.map(row =>
headers.map(header => {
const value = row[header] === null || row[header] === undefined ? '' : String(row[header]);
// Escape double quotes and wrap in double quotes if necessary
return `"${value.replace(/"/g, '""')}"`;
}).join(',')
);
return [headers.join(','), ...rows].join('\n');
}
// Helper function to download the CSV
function downloadCSV(csvString, fileName) {
const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
if (link.download) {
link.setAttribute('href', URL.createObjectURL(blob));
link.setAttribute('download', fileName);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
function CsvExporter(props) {
const [isLoading, setIsLoading] = createSignal(false);
const [selectedData, setSelectedData] = createStore([]);
const [availableColumns, setAvailableColumns] = createSignal([]);
const [selectedColumns, setSelectedColumns] = createSignal([]);
const [fileName, setFileName] = createSignal(props.fileName || 'export.csv');
// Simulate fetching data (replace with actual API call or WordPress REST API)
const fetchData = async () => {
setIsLoading(true);
// Example: Fetching product data from a custom endpoint
try {
const response = await fetch('/wp-json/my-plugin/v1/products'); // Replace with your actual endpoint
const data = await response.json();
if (data && data.length > 0) {
const columns = Object.keys(data[0]);
setAvailableColumns(columns);
setSelectedColumns(columns); // Select all by default
setSelectedData(data);
} else {
setSelectedData([]);
setAvailableColumns([]);
setSelectedColumns([]);
}
} catch (error) {
console.error("Error fetching data:", error);
setSelectedData([]);
setAvailableColumns([]);
setSelectedColumns([]);
} finally {
setIsLoading(false);
}
};
const handleExport = () => {
if (selectedData.length === 0 || selectedColumns.length === 0) {
alert('No data or columns selected for export.');
return;
}
const csvString = convertToCSV(selectedData, selectedColumns);
downloadCSV(csvString, fileName());
};
// Effect to fetch data when component mounts (or when a prop changes)
// In a real Gutenberg block, you might fetch data based on block attributes or context
// For simplicity, we'll call it directly here.
// Consider using useEffect from 'solid-js/web' for side effects in a React-like way if needed.
// For this example, we'll trigger it via a button.
return (
<div>
<h3>CSV Exporter</h3>
<button onClick={fetchData} disabled={isLoading()}>{isLoading() ? 'Loading...' : 'Load Data'}</button>
{availableColumns().length > 0 && (
<div>
<h4>Select Columns to Export:</h4>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', marginBottom: '20px' }}>
<For each={availableColumns()}>
{(column) => (
<label>
<input
type="checkbox"
checked={selectedColumns().includes(column)}
onChange={(e) => {
if (e.target.checked) {
setSelectedColumns([...selectedColumns(), column]);
} else {
setSelectedColumns(selectedColumns().filter(c => c !== column));
}
}}
/>
{column}
</label>
)}
</For>
</div>
<div style={{ marginBottom: '20px' }}>
<label htmlFor="fileName">File Name: </label>
<input
type="text"
id="fileName"
value={fileName()}
onInput={(e) => setFileName(e.target.value)}
style={{ padding: '8px', border: '1px solid #ccc', borderRadius: '4px' }}
/>
</div>
<button onClick={handleExport} disabled={selectedData.length === 0 || selectedColumns().length === 0}>
Export Selected CSV
</button>
</div>
)}
{selectedData.length === 0 && !isLoading() && (
<p>Click "Load Data" to fetch records for export.</p>
)}
</div>
);
}
export default CsvExporter;
Integrating SolidJS with Gutenberg
To render the SolidJS component within the Gutenberg editor, we’ll use the @wordpress/element package, which provides a compatibility layer for React-like frameworks. We’ll create an editor.js file that imports our SolidJS component and uses wp.element.createElement to render it.
src/editor.js
import { registerBlockType } from '@wordpress/blocks';
import { createElement } from '@wordpress/element';
import CsvExporter from './components/CsvExporter'; // Assuming CsvExporter.jsx is in src/components/
// Import SolidJS runtime if not globally available or handled by bundler
// import { render } from 'solid-js/web'; // This might be needed depending on your setup
registerBlockType('my-csv-exporter/csv-exporter', {
title: 'High-Performance CSV Exporter',
icon: 'download',
category: 'widgets',
attributes: {
data: {
type: 'string',
default: '',
},
fileName: {
type: 'string',
default: 'export.csv',
},
},
edit: (props) => {
// Use createElement to render the SolidJS component.
// The 'props' object contains block attributes and other Gutenberg context.
// We pass relevant props to our SolidJS component.
return createElement(CsvExporter, {
fileName: props.attributes.fileName,
// You might pass a function to update attributes if needed
// onFileNameChange: (newFileName) => props.setAttributes({ fileName: newFileName }),
});
},
save: (props) => {
// The save function should return null for dynamic blocks or
// static HTML. For a block that primarily performs client-side actions
// and doesn't render persistent UI in the frontend, returning null is common.
// If you need to save attributes like fileName, you can render them here.
return null; // Or return a placeholder if needed
},
});
In the edit function, we use createElement from @wordpress/element. This function acts as a bridge, allowing us to instantiate our SolidJS component within the WordPress editor’s React-based virtual DOM. We pass the block’s attributes (like fileName) as props to our CsvExporter component.
The save function returning null signifies a dynamic block. This means the block’s rendering on the frontend will be handled by PHP (or a REST API endpoint), which can fetch data and generate the CSV on demand, or it might rely entirely on the script.js for frontend interactivity if the export is triggered directly by the user on the live site.
Frontend Script (Optional but Recommended)
While the editor component handles the UI and export logic, you might want a similar or simplified export trigger on the frontend. The script.js file is enqueued for the frontend and can contain logic to initialize the block or provide frontend-specific functionality.
src/script.js
// This script runs on the frontend.
// If your block is dynamic, this might not be strictly necessary for the export itself,
// but it's good practice to have it for potential frontend interactions or
// if you decide to render the export button directly on the page.
console.log('My CSV Exporter frontend script loaded.');
// Example: If you wanted to initialize the component on the frontend
// document.addEventListener('DOMContentLoaded', () => {
// const exporterElements = document.querySelectorAll('.wp-block-my-csv-exporter-csv-exporter');
// exporterElements.forEach(element => {
// // You would need a way to render SolidJS components outside of Gutenberg's context here.
// // This typically involves a separate SolidJS entry point and rendering logic.
// // For this example, we assume the export is primarily an editor function or
// // handled by a dynamic block rendering.
// });
// });
Backend API Endpoint (Example)
The SolidJS component’s fetchData function uses a placeholder REST API endpoint (/wp-json/my-plugin/v1/products). You’ll need to create this endpoint in your WordPress plugin’s PHP file to serve the data. This endpoint should return a JSON array of objects, where each object represents a row and keys are column names.
<?php
// Add this to your main plugin file (my-csv-exporter.php)
add_action( 'rest_api_init', function () {
register_rest_route( 'my-plugin/v1', '/products', array(
'methods' => 'GET',
'callback' => 'my_csv_exporter_get_products_data',
'permission_callback' => '__return_true', // Adjust permissions as needed
) );
} );
function my_csv_exporter_get_products_data( WP_REST_Request $request ) {
// --- IMPORTANT ---
// Replace this with your actual data fetching logic.
// This could involve querying the 'posts' table, WooCommerce products,
// custom post types, or any other data source.
// Ensure the data is structured as an array of associative arrays.
// Example: Fetching published posts (as a simple demonstration)
$args = array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => -1, // Fetch all posts
);
$posts = get_posts( $args );
$data = array();
if ( ! empty( $posts ) ) {
foreach ( $posts as $post ) {
$data[] = array(
'ID' => $post->ID,
'Title' => $post->post_title,
'Date' => $post->post_date,
'Author ID' => $post->post_author,
'Status' => $post->post_status,
// Add more fields as needed
);
}
}
return new WP_REST_Response( $data, 200 );
}
?>
This REST API endpoint provides the data in JSON format, which our SolidJS component can easily consume. For large datasets, consider implementing pagination or server-side filtering within this endpoint to avoid overwhelming the browser.
Build Process and Deployment
After setting up your files, you’ll need to run the build command defined in your package.json. If using Vite, you might have a script like:
"scripts": {
"dev": "vite",
"build": "vite build"
}
Run npm run build to compile your SolidJS components and JavaScript into the build directory. This directory should then be included in your plugin’s deployment package. Ensure the index.asset.php file is also generated and placed correctly if your build process doesn’t handle it automatically.
Performance Considerations and Enhancements
SolidJS’s fine-grained reactivity minimizes unnecessary re-renders, making it performant for complex UIs. However, for extremely large datasets:
- Data Fetching: Optimize the backend API endpoint. Use efficient database queries, consider caching, and implement server-side pagination or lazy loading if the dataset is massive.
- CSV Generation: While client-side generation is fast for moderate data, for millions of rows, consider a hybrid approach where the initial data is fetched client-side, but for very large exports, a server-side PHP process is triggered (perhaps via an AJAX call to a different endpoint) to generate the CSV directly on the server and offer it for download.
- Column Selection: For hundreds of columns, the checkbox list can become unwieldy. Implement search functionality or a multi-select component for better usability.
- Memory Management: Be mindful of browser memory limits when processing very large datasets client-side. If the JSON payload becomes too large, it can crash the browser tab.
By adopting SolidJS and a well-structured Gutenberg block architecture, you can create a powerful and performant CSV export tool that significantly enhances the workflow for e-commerce managers and technical teams.