Search Params
Query strings are how a filter, a tab or a page number survives a reload and a shared link. Prisma PHP does not wrap them in a bespoke API:
you use the platform's URLSearchParams
in the browser and Request::$params
on the server.
Read and build the query string. Standard platform API, no wrapper.
Apply it as a navigation, so the route re-renders with the new params.
React when the query string changes underneath a component.
Reading a parameter
<script>
const readQuery = () =>
new URLSearchParams(location.search).get('q') ?? '';
const [query, setQuery] = pp.state(readQuery);
</script>
Passing the function itself to pp.state makes it a lazy initializer: it runs once, on first render.
<?php
use PP\Request;
use PP\Validator;
$query = Validator::string(Request::$params->q ?? '');
$page = max(1, Validator::int(Request::$params->page ?? 1));
?>
Request::$params is an ArrayObject, so read it with property access.
Always run untrusted values through Validator.
Writing a parameter
There are two distinct intents, and choosing the wrong one is the usual source of confusion.
Navigate — the server should see it
Use pp.redirect(url). Same-origin URLs go through SPA navigation, so the route re-renders with the
new Request::$params and the URL becomes shareable and bookmarkable.
<script>
function applyFilter(status) {
const params = new URLSearchParams(location.search);
if (status) params.set('status', status);
else params.delete('status');
params.delete('page'); // reset pagination
const qs = params.toString();
pp.redirect(location.pathname + (qs ? '?' + qs : ''));
}
</script>
Record — keep the UI as it is
Use history.replaceState(...) when the state already lives in
pp.state and you only want the address bar to reflect it — a deep-linkable tab, an open
dialog, a scroll position. No request is made and nothing re-renders.
<script>
const [tab, setTab] = pp.state('overview');
function selectTab(next) {
setTab(next);
const params = new URLSearchParams(location.search);
params.set('tab', next);
history.replaceState(
history.state, '',
location.pathname + '?' + params.toString()
);
}
</script>
Reacting to a change
Two things can change the query string behind your back: an SPA navigation elsewhere on the page, and the browser's back and forward buttons. Listen for both.
<script>
const readStatus = () =>
new URLSearchParams(location.search).get('status') ?? 'all';
const [status, setStatus] = pp.state(readStatus);
pp.effect(() => {
const sync = () => setStatus(readStatus());
document.addEventListener('pp:navigation:complete', sync);
window.addEventListener('popstate', sync);
return () => {
document.removeEventListener('pp:navigation:complete', sync);
window.removeEventListener('popstate', sync);
};
}, []);
</script>
Request::$params in PHP usually needs no listener at all. Reach for this pattern when a
component has to react without the whole route changing.
Filters without a round trip
When the filter should feel instant and the URL is only there for sharing, combine the two: query the server with
pp.rpc and record the state with
history.replaceState.
<script>
const [query, setQuery] = pp.state('');
const [results, setResults] = pp.state([]);
async function search(value) {
setQuery(value);
const params = new URLSearchParams(location.search);
value ? params.set('q', value) : params.delete('q');
history.replaceState(history.state, '', location.pathname + '?' + params.toString());
const response = await pp.rpc('searchPosts', { query: value }, { abortPrevious: true });
if (response?.cancelled) return;
setResults(response.items ?? []);
}
</script>