Build your own island

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.

1. The data, in PHP

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' ];

?>

2. The island, in the template

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>

3. The component, in www/

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);

4. Talking back

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);

The runtime

www/react/pad-react.js - plain JavaScript, loaded by _inits.pad on every page.

CallWhat 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.

Good to know

Keep JSX out of templates

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}.

Values are text

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.

Server first

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.

Going to production

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.