Four files make a page with React in it: the data, the template, the component - and the runtime every page already has. Then the same page can answer the component's questions.
The page's PHP gathers what the component starts with - from the database, a file in
_data/, the session. Nothing about React here.
<?php
// orders.php - the first data of the component
$orders = db ( "ARRAY id, customer, total, status FROM orders ORDER BY id DESC" );
$filter = [ 'status' => 'open' ];
?>
An element with data-island names the component; {^orders} writes the
field as JSON, escaped for the attribute. What stands inside shows until React takes over.
<h1>Orders</h1>
<div data-island="OrderBoard" data-props="{^orders}">
{orders}<p>{$customer} - {$total}</p>{/orders}
</div>
<script type="text/babel" src="{$padRoot}react/orders.js"></script>
A plain file under www/react/, served as it is - so its braces never meet the PAD parser.
PadReact.island mounts it on every island of that name, the JSON as its props.
// www/react/orders.js - the props are the JSON PAD wrote; a list arrives as data
function OrderBoard({ data }) {
const [orders, setOrders] = React.useState(data);
const [status, setStatus] = React.useState('all');
const shown = orders.filter(order => status === 'all' || order.status === status);
return (
<div>
<select value={status} onChange={event => setStatus(event.target.value)}>
<option value="all">All</option>
<option value="open">Open</option>
</select>
{shown.map(order => <p key={order.id}>{order.customer} - {order.total}</p>)}
</div>
);
}
PadReact.island('OrderBoard', OrderBoard);
The page answers JSON when asked - but only what $padExpose names. The templates do not
run for such a request. A post needs the session's CSRF token, which PadReact.post
sends in a header when $padCsrf is on.
// orders.php - one more line
$padExpose = [ 'orders' ];
// a post from the component
if ( padRequestIs ( 'POST' ) ) {
$errors = padValidate ( [ 'qty' => 'required|integer|min:1' ] );
if ( ! $errors )
db ( "UPDATE orders SET qty={0} WHERE id={1}", [ $qty, $id ] );
}
From the component it is one call each - and {fragment 'name'} in the template
lets a component fetch one rendered part of a page as HTML.
// ?orders&padFormat=json
const { orders } = await PadReact.get('orders', { status: 'open' });
// POST ?orders&padFormat=json, X-CSRF-Token: ...
const answer = await PadReact.post('orders', { id: 7, qty: 2 });
// ?orders&padFragment=summary - HTML
const html = await PadReact.fragment('orders', 'summary');
// as a hook: asked again whenever status changes
const { data, loading } = PadReact.useData('orders', { status }, first);
www/react/pad-react.js - plain JavaScript, loaded by _inits.pad on every page.
| Call | What it does |
|---|---|
PadReact.island(name, Component) | Renders Component on every [data-island=name], with the JSON of data-props as props (a list arrives as props.data) and what PAD rendered inside as props.serverHtml. |
PadReact.get(page, params) | The page's exposed data: ?page&...&padFormat=json, parsed. |
PadReact.post(page, body, { token }) | Posts the body form-encoded with the X-CSRF-Token header from <meta name="csrf-token">; token: false leaves it out. |
PadReact.fragment(page, name, params) | One {fragment} of the page as HTML. |
PadReact.useData(page, params, first) | A hook: { data, loading, error }, asked again when the params change, the request before aborted. |
PadReact.useDebounced(value, ms) | The value once it stood still for ms - for a search box. |
A request that does not succeed throws an error with status and the parsed body. Every request is
told to the page as a pad:request event - the request log under the examples listens to it.
PAD reads every { } of a template. Components live in www/, loaded with
<script type="text/babel" src>; for a short inline script there is {ignore}.
A field in data-props is escaped for the attribute and never runs as PAD, whatever a visitor typed
into it - React escapes it once more when it renders.
Give the island its first data in the props and put real HTML inside it: the page shows something before any script ran, and search engines see it.
Babel in the browser is for learning. Compile the components with esbuild or Vite into one file, load the
production React builds, and drop type="text/babel" - the PAD side stays as it is.