This file documents patterns for integrating React with PAD templates.
When integrating React with PAD, the key principle is separation of concerns:
Use this pattern for data stored in JSON files in _data/.
_data/nav.json:
[
{ "page": "index", "label": "Home", "icon": "π " },
{ "page": "about", "label": "About", "icon": "π" }
]
_inits.pad:
<nav>
{local:nav.json}
<a href="?{$page}" {if $padPage == $page}class="active"{/if}>
{$icon} {$label}
</a>
{/local:nav.json}
</nav>
_tags/json.php:
<?php
// Read JSON file from _data/, compact and HTML-escape for attributes
$jsonContent = file_get_contents(APP . "_data/$padParm.json");
$jsonData = json_decode($jsonContent, true);
$jsonCompact = json_encode($jsonData);
$padContent = htmlspecialchars($jsonCompact, ENT_QUOTES, 'UTF-8');
return TRUE;
?>
template.pad:
<div id="app" data-products="{json 'products' | ignore}"></div>
{ignore}<script>
const products = JSON.parse(document.getElementById('app').dataset.products);
// React can now use products
</script>{/ignore}
When the data is already a field of the page - set in the pageβs .php, or a field of the
row a loop is on - the ^ sigil writes it as JSON, escaped for a double-quoted attribute,
with no tag of your own and no | ignore:
product.php:
<?php
$product = [ 'name' => 'Chair', 'price' => 49.5, 'tags' => [ 'wood', 'brown' ] ];
?>
product.pad:
<div id="product" data-props="{^product}"></div>
An array field is encoded whole; a scalar becomes a JSON string or number. Read it in the
component with JSON.parse(elem.getAttribute('data-props')).
Use this pattern for database-driven or dynamic data.
apps/myapp/
βββ _providers/ # PHP data providers
βββ topic.php # Returns topic record
βββ user.php # Returns user record
βββ posts.php # Returns posts array
{reactData} is a built-in tag (pad/tags/reactData.php) - an application writes no tag of
its own for it, and a _tags/reactData.php would replace the built-in one, since application
tags are found first. It runs _providers/<provider>.php (provider= defaults to id=),
keeps the result in $padProviders[<id>] for the providers after it, and writes
<div id="<id>" data="<JSON>"> with the JSON escaped for the attribute. type='check' turns
the result into 1 or 0. The engineβs $padData is not a name a provider or tag may assign.
_providers/topic.php (single record):
<?php
// Providers return data - they have access to all variables from the page
return db("RECORD * FROM forum_topics WHERE id={0}", [$id]);
?>
_providers/posts.php (array):
<?php
// IMPORTANT: Use array_values() to ensure proper JSON array (not object with numeric keys)
$posts = db("ARRAY * FROM forum_posts WHERE topic_id={0}", [$id]);
return array_values($posts);
?>
topic.pad:
<h1>Forum Topic</h1>
<!-- Multiple data sources - each with unique ID -->
{reactData id='topic', provider='topic', $id=$id}
{reactData id='board', provider='board', $boardId=$boardId}
{reactData id='user', provider='user', $userId=$userId}
{reactData id='posts', provider='posts', $id=$id}
<div id="react-app"></div>
<script type="text/babel" src="/react/examples/topic.js"></script>
// β WRONG - dataset only works for data-* attributes, returns undefined for plain "data"
const topicElem = document.getElementById('topic');
const topic = JSON.parse(topicElem.dataset.data); // FAILS!
// β
CORRECT - Use getAttribute() for plain "data" attribute
const topicElem = document.getElementById('topic');
const topic = JSON.parse(topicElem.getAttribute('data')); // WORKS!
www/react/examples/topic.js:
function TopicDisplay() {
// Get data from all reactData divs
// IMPORTANT: Use getAttribute('data') NOT dataset.data!
const topicElem = document.getElementById('topic');
const boardElem = document.getElementById('board');
const userElem = document.getElementById('user');
const postsElem = document.getElementById('posts');
const topic = JSON.parse(topicElem.getAttribute('data'));
const board = JSON.parse(boardElem.getAttribute('data'));
const user = JSON.parse(userElem.getAttribute('data'));
const posts = JSON.parse(postsElem.getAttribute('data'));
return (
<div className="topic-display">
<div className="breadcrumb">
<a href="?forum/index">Forum</a> β
<a href={`?forum/board&id=${board.id}`}>{board.name}</a>
</div>
<h1>{topic.title}</h1>
<div className="topic-meta">
Posted by {user.username} on {new Date(topic.created_at).toLocaleDateString()}
</div>
<div className="posts">
{posts.map((post, index) => (
<div key={post.id} className="post">
<div className="post-header">
Post #{index + 1} by {post.username}
</div>
<div className="post-content">
{post.content}
</div>
</div>
))}
</div>
</div>
);
}
// Render the component
const root = ReactDOM.createRoot(document.getElementById('react-app'));
root.render(<TopicDisplay />);
A component that fetches its data after the page loaded does not need a provider: any page
answers its data as JSON when it names what may leave the server in $padExpose.
// orders.php - the same page renders orders.pad for a browser
$orders = db ( "ARRAY * FROM orders" );
$padExpose = [ 'orders' ];
fetch('?orders&padFormat=json').then(r => r.json()).then(data => setOrders(data.orders));
// or: fetch('?orders', { headers: { Accept: 'application/json' } })
The pageβs first answer can go into the props with {^orders}, so the component shows data
at once and asks again only when the visitor changes something - the same PHP serves both.
The react application (apps/react/, www/react/pad-react.js) mounts its components on
islands: an element naming the component, its props as JSON in data-props, and HTML of
PADβs own inside that shows until React takes the element over.
<!-- props.pad - one island per row of a PAD loop -->
{members}
<div data-island="MemberCard" data-props="{^card}">
<strong>{$name}</strong>
</div>
{/members}
// www/react/examples/props.js
function MemberCard({ name, role, skills }) { ... }
PadReact.island('MemberCard', MemberCard); // every [data-island=MemberCard], its own root
Each island is its own React root with its own state. The runtime passes what PAD rendered
inside as props.serverHtml - a component can keep it, as the chart example keeps the SVG.
Posts with the CSRF token. With $padCsrf on, a post without the sessionβs token is
answered 403 before the page runs. Write the token where a script can read it and send it in
the X-CSRF-Token header; the page validates as for any form and answers JSON.
<meta name="csrf-token" content="{csrf token}">
// feedback.php
if ( padRequestIs ( 'POST' ) )
$errors = padValidate ( [ 'email' => 'required|email', 'message' => 'required|min:10' ] );
$padExpose = [ 'errors', 'wall' ];
fetch('?feedback&padFormat=json', {
method: 'POST',
headers: { 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content },
body: new URLSearchParams(form)
});
One rendered part. {fragment 'chart'}...{/fragment} in a template is answered alone to
?page&padFragment=chart - a component can let PAD render what it does well (a {chart}, a
{markdown} text, a table) and put the HTML in place.
State on the server. Keep what several islands share in the session (padSession,
padSessionPut) and let every change answer the whole state; one island tells the others
with an event on window. A reload starts every island from the session again.
Polling. A component can ask a pageβs JSON on an interval - stop it while
document.hidden is true, so a forgotten tab does not keep the server busy.
| Principle | Description |
|---|---|
| Server-side data | Use _data/*.json files (static) or _providers/*.php (dynamic) |
| PAD responsibility | Data preparation and HTML structure |
| React responsibility | Client-side interactivity |
| JSON in attributes | Use \| ignore pipe with {json} tag |
| JavaScript blocks | Wrap in {ignore}...{/ignore} tags |
| Data attribute access | Use getAttribute('data') NOT dataset.data |
| JSON arrays | Use array_values() in providers for proper arrays |
| Unique IDs | Each {reactData} needs unique id parameter |
| Variable access | Providers have access to all page variables |
PAD parses { } as tags. When working with JavaScript/React:
{ignore}
<script>
const user = { name: 'Alice', role: 'Developer' };
if (user.active) { console.log('Active'); }
</script>
{/ignore}
<script src="/js/app.js"></script>
<script type="text/babel" src="/react/components/MyComponent.js"></script>
<div data-config="{echo $configJson | html | ignore}"></div>
html escapes the JSONβs quotes for the attribute - {echo} does not - and ignore keeps PAD
off the braces. For an array field data-config="{^config}" does both:
<div data-config="{^config}"></div>
Same principle applies - prefer external CSS files:
<link rel="stylesheet" href="/css/react-components.css">
Or use {ignore} for inline styles:
{ignore}
<style>
.topic-display { padding: 20px; }
.post { margin: 10px 0; }
</style>
{/ignore}
Recommended structure for React integration:
apps/myapp/
βββ _data/ # Static JSON data
β βββ nav.json
β βββ config.json
βββ _providers/ # Dynamic data providers
β βββ user.php
β βββ posts.php
βββ _tags/
β βββ json.php # {json} tag for static data - {reactData} is built in
βββ pages/
βββ forum/
βββ topic.pad
www/
βββ react/ # React components
β βββ topic/
β βββ display.js
βββ js/ # Plain JavaScript
βββ css/ # Stylesheets
function MyComponent() {
const dataElem = document.getElementById('my-data');
if (!dataElem) {
return <div>Loading...</div>;
}
const data = JSON.parse(dataElem.getAttribute('data'));
return <div>{/* render data */}</div>;
}
function MyComponent() {
const dataElem = document.getElementById('my-data');
try {
const data = JSON.parse(dataElem.getAttribute('data'));
return <div>{data.title}</div>;
} catch (e) {
return <div>Error loading data</div>;
}
}
{reactData id='users', provider='users'}
{reactData id='roles', provider='roles'}
{reactData id='permissions', provider='permissions'}
<div id="admin-panel"></div>
function AdminPanel() {
const users = JSON.parse(document.getElementById('users').getAttribute('data'));
const roles = JSON.parse(document.getElementById('roles').getAttribute('data'));
const permissions = JSON.parse(document.getElementById('permissions').getAttribute('data'));
// Combine data as needed
return <div>{/* admin UI */}</div>;
}