---
title: "Migrating from State Pages · RUAL Documentation"
description: "Move a state page to web blocks: which block replaces which, how state and modals become templates and actions, and a safe order to do it in."
canonical: https://docs.rual.nl/web/migrate-from-state-pages
language: en
---

# Migrating from State Pages

Move one page at a time: build the web page at a new URL next to the old one, compare the two, then move the URL over. State pages and UI components keep running, so nothing has to happen at once. The tables below show which web block replaces which, and what has no direct replacement.

In short

- A component tree becomes a template. Client state becomes server values, sent back as HTML. Event handlers become actions.

- Your storages, functions, translations and scopes stay as they are. Web pages use the same ones.

- Live push over a WebSocket, the built-in datatable and custom React components have no one-to-one replacement.

- Start with a page that mostly shows things, and keep the old blueprint until the new one has proven itself.

## What actually changes

|  | State page | Web page |
| --- | --- | --- |
| What the browser gets | A React application that builds the page | Finished HTML and a script under 4 KB compressed |
| The markup comes from | A tree of blocks, one per element | A template, usually one block |
| Interaction runs over | A WebSocket, with state held between requests | A plain `POST`. The server holds nothing between requests |
| Typing into a form | Goes through state | Stays in the browser |
| An answer holds | State updates the page reacts to | Pieces of HTML to put in place |

## Which web block replaces which

### Pages and rendering

| State or UI component block | Becomes |
| --- | --- |
| [`state_page`](https://docs.rual.nl/block-types/state%20ui/state_page), [`uicomponent_page`](https://docs.rual.nl/block-types/User%20Interface/uicomponent_page) | [`web_page`](https://docs.rual.nl/block-types/web/web_page). The same `event`, `scopes` and `l10n`, and the same `connection`, `params` and `query` pins. |
| [`state_render_page`](https://docs.rual.nl/block-types/state%20ui/state_render_page), [`uicomponent_page_render`](https://docs.rual.nl/block-types/User%20Interface/uicomponent_page_render) | [`web_render`](https://docs.rual.nl/block-types/web/web_render), with a `template` where the `content` pin was. |
| [`state_div`](https://docs.rual.nl/block-types/state%20ui/state_div), [`state_h1`](https://docs.rual.nl/block-types/state%20ui/state_h1), [`state_button`](https://docs.rual.nl/block-types/state%20ui/state_button) and every other element block | The plain tag in the template: ` `, ` `, ` `. |
| [`state_html`](https://docs.rual.nl/block-types/state%20ui/state_html) | Plain markup in the template, or `{{{value}}}` for HTML a flow built. |
| State templates in **Manage Templates** | A stored `web` template used as a [layout](https://docs.rual.nl/web/layouts-and-partials#layouts), with `{{{body}}}` and `{{{head}}}`. |
| [`uicomponent_update_meta_tags`](https://docs.rual.nl/block-types/User%20Interface/uicomponent_update_meta_tags) | The `title`, `description`, `image` and `meta` pins. See [SEO and Sharing](https://docs.rual.nl/web/seo). |

### Lists and tables

| State or UI component block | Becomes |
| --- | --- |
| [`state_foreach_div`](https://docs.rual.nl/block-types/state%20ui/state_foreach_div), [`state_foreach_ul`](https://docs.rual.nl/block-types/state%20ui/state_foreach_ul), [`state_foreach_ol`](https://docs.rual.nl/block-types/state%20ui/state_foreach_ol), [`state_foreach_tr`](https://docs.rual.nl/block-types/state%20ui/state_foreach_tr), [`state_foreach_tbody`](https://docs.rual.nl/block-types/state%20ui/state_foreach_tbody), [`state_foreach_thead`](https://docs.rual.nl/block-types/state%20ui/state_foreach_thead), [`uicomponent_foreach`](https://docs.rual.nl/block-types/user%20interface/uicomponent_foreach) | [`web_foreach`](https://docs.rual.nl/block-types/web/web_foreach) when an action changes the rows later, or `{{for item in items}}` in the template when they only show. See [Lists and Tables](https://docs.rual.nl/web/lists#foreach-or-for). |
| [`state_datatable`](https://docs.rual.nl/block-types/state%20ui/state_datatable), [`state_advanced_datatable`](https://docs.rual.nl/block-types/state%20ui/state_advanced_datatable) | [`web_foreach`](https://docs.rual.nl/block-types/web/web_foreach) into a ` `, plus an action for each thing the table does. See [below](#no-replacement). |
| [`state_set_datatable_refresh`](https://docs.rual.nl/block-types/state%20ui/state_set_datatable_refresh), [`state_datatable_push`](https://docs.rual.nl/block-types/state%20ui/state_datatable_push) | [`web_replace`](https://docs.rual.nl/block-types/web/web_replace) and [`web_insert`](https://docs.rual.nl/block-types/web/web_insert) of just that row. |

### Events and state

| State or UI component block | Becomes |
| --- | --- |
| [`stateevents_onclick`](https://docs.rual.nl/block-types/state%20events/stateevents_onclick), [`stateevents_onchange`](https://docs.rual.nl/block-types/state%20events/stateevents_onchange), [`stateevents_onenter`](https://docs.rual.nl/block-types/state%20events/stateevents_onenter) | `data-action="name"` on the element, and a [`web_action`](https://docs.rual.nl/block-types/web/web_action) with that name. A button or link fires on click, a field on change, and a form on submit, Enter included. |
| [`state_on_state_change`](https://docs.rual.nl/block-types/state%20ui/state_on_state_change) | A field with `data-action`, adding `data-trigger="input"` to run while typing. |
| [`state_trigger_custom_function`](https://docs.rual.nl/block-types/globals%2Cstate%20execution/state_trigger_custom_function) and its [`state_inline_function_execute`](https://docs.rual.nl/block-types/function%20execution/state_inline_function_execute) | A [`web_action`](https://docs.rual.nl/block-types/web/web_action). A shared piece of logic becomes an ordinary function that the action calls. |
| [`state_backend_request`](https://docs.rual.nl/block-types/state%20ui/state_backend_request) | Nothing: the action is already on the server. To fill a part of the page after it loads, use `data-trigger="load"`. |
| [`state_update_state`](https://docs.rual.nl/block-types/state%20ui/state_update_state), [`state_update_state_fields`](https://docs.rual.nl/block-types/state%20ui/state_update_state_fields), [`state_return_state`](https://docs.rual.nl/block-types/state%20ui/state_return_state), [`state_increment_state`](https://docs.rual.nl/block-types/state%20ui/state_increment_state), global state | A value on the server, usually in a storage, and a [`web_update`](https://docs.rual.nl/block-types/web/web_update) or [`web_replace`](https://docs.rual.nl/block-types/web/web_replace) of the place that shows it. Read [the section below](#state-to-html). |
| [`state_is_true`](https://docs.rual.nl/block-types/state%20ui/state_is_true), [`state_is_empty`](https://docs.rual.nl/block-types/state%20ui/state_is_empty), [`state_branch`](https://docs.rual.nl/block-types/state%20ui/state_branch) | `{{if}}` in the template, or a branch in the flow when the server decides. |

### Forms, navigation and the rest

| State or UI component block | Becomes |
| --- | --- |
| [`state_form`](https://docs.rual.nl/block-types/state%20ui/state_form), [`state_input`](https://docs.rual.nl/block-types/state%20ui/state_input), [`state_submit_button`](https://docs.rual.nl/block-types/state%20ui/state_submit_button) | A plain ` ` with plain inputs. See [Forms and Validation](https://docs.rual.nl/web/forms). |
| [`state_form_get`](https://docs.rual.nl/block-types/state%20ui/state_form_get) | The `form` out-pin of [`web_action`](https://docs.rual.nl/block-types/web/web_action). |
| Field validation shown in the form | `required`, `pattern` and the like in the markup, and [`web_errors`](https://docs.rual.nl/block-types/web/web_errors) for what only the server knows. |
| [`state_flow_modal`](https://docs.rual.nl/block-types/state%20ui/state_flow_modal), [`state_return_modal`](https://docs.rual.nl/block-types/state%20ui/state_return_modal), [`state_close_flow_modal`](https://docs.rual.nl/block-types/state%20ui/state_close_flow_modal), [`uicomponent_modal`](https://docs.rual.nl/block-types/User%20Interface/uicomponent_modal) | Markup that is on the page already, opened with `data-toggle="#dialog"` with no request. For content the server must build, either a page of its own, or an action that answers [`web_replace`](https://docs.rual.nl/block-types/web/web_replace) of a hidden placeholder with the visible dialog. |
| [`state_translate_key`](https://docs.rual.nl/block-types/translations/state_translate_key), [`state_translate_dynamic`](https://docs.rual.nl/block-types/translations/state_translate_dynamic) | `{{t "key"}}` and `{{t key}}`. Your translation keys stay as they are. See [Languages](https://docs.rual.nl/web/languages#translations). |
| [`state_redirect`](https://docs.rual.nl/block-types/state%20ui/state_redirect), [`state_redirect_to_page`](https://docs.rual.nl/block-types/state%20ui/state_redirect_to_page), [`uicomponent_go_to_page`](https://docs.rual.nl/block-types/User%20Interface/uicomponent_go_to_page) | [`web_redirect`](https://docs.rual.nl/block-types/web/web_redirect), with a path you write out. |
| [`state_refresh_page`](https://docs.rual.nl/block-types/state%20ui/state_refresh_page) | A [`web_redirect`](https://docs.rual.nl/block-types/web/web_redirect) to the page's own path, or a [`web_render`](https://docs.rual.nl/block-types/web/web_render) in the action. |
| [`state_script`](https://docs.rual.nl/block-types/state%20ui/state_script), [`state_custom_function_execution`](https://docs.rual.nl/block-types/state%20ui/state_custom_function_execution) | Your own ` `, or a file the layout links. |
| [`state_set_cookie`](https://docs.rual.nl/block-types/state%20ui/state_set_cookie) | A `Set-Cookie` in the `headers` pin of [`web_render`](https://docs.rual.nl/block-types/web/web_render), or a line in your script. Reading stays [`httpconnection_get_cookie`](https://docs.rual.nl/block-types/http%20connection/httpconnection_get_cookie), which reads the `ui-` prefix for you. |
| [`state_input_file`](https://docs.rual.nl/block-types/state%20ui/state_input_file), [`state_get_uploaded_file`](https://docs.rual.nl/block-types/state%20ui/state_get_uploaded_file) | The [upload endpoints](https://docs.rual.nl/tutorials/file-upload-processing), with the result passed to the action. A web action refuses files. |

## State becomes a value on the server and a piece of HTML

This is the one change in thinking. On a state page the browser holds state and the page reacts to it. On a web page the page shows what the server rendered, and an action changes something on the server and sends back what now looks different.

#### State page

A click updates `cart_count` in global state, and every component that reads it renders again.

#### Web page

A click runs an action that writes the cart to a storage and answers `web_update(target "#cart-count", html: count)`. If two places show the count, the action answers two updates, or the places share one element.

Purely visual state needs no server at all: `data-toggle` for show and hide, and a few lines of your own script for the rest.

## What has no one-to-one replacement

| On a state page | On a web page |
| --- | --- |
| **Live updates pushed over a WebSocket**, such as [`state_broadcast_subscribe`](https://docs.rual.nl/block-types/state/state_broadcast_subscribe) | A web page has no WebSocket of its own. Poll with `data-trigger="every 10s"`, which pauses while the page is not visible. For true push, open a socket in your own script and call `RUALWeb.apply(ops)`. A chat or a live dashboard may be better off staying a state page for now. |
| **The built-in datatable:** sorting, filtering, paging and selection out of the box | You write what you need: a search with a sort from `query` or a `data-param-*`, links or an action for paging, and checkboxes in a form for selection. It is more work up front, and every part is plain HTML you control. A heavy admin table is a fair thing to migrate last. |
| **Client state shared between components** | Rendered by the server, or held in your own script. A page that behaves like a spreadsheet or an editor is mostly client state, and suits this model least. |
| **Custom React components** | Your own script with a nonce, mounted on an element in the template. There is no hydration step. You bring and load the library yourself, from your own files or over `https:`. |
| **Modals as flows of their own,** opened by name from anywhere | No registry of modals. A dialog is markup on the page, a page, or the answer of an action in this blueprint. |
| **Actions across blueprints** | An action belongs to its page's blueprint. Put shared logic in a function and call it from an action in each. |

## A safe order to move one page in

- Write down what the page does

List what it reads, what each click and form does, who may open it, and what it shows search engines. This list is your test later. Start with a page that mostly shows things, and leave the most interactive ones for last.

- Build the web page at a new URL

Make a new blueprint with a [`web_page`](https://docs.rual.nl/block-types/web/web_page) at a URL nobody uses yet, such as `new/pricing`, with the same scopes as the old page. Set both in the [code view](https://docs.rual.nl/engine/studio-code-view), as in [Your First Page](https://docs.rual.nl/web/first-page), since the canvas has no URL button on this block yet. Keep this URL out of search with the `meta` pin: `{"robots": "noindex"}`. The old page is untouched, so nothing can break yet.

- Move what it shows first

Turn the shell into a stored layout and the elements into a template, and call the same functions and storages. Compare the rendered result with the old page, in each language and both signed in and signed out.

- Then its actions, one at a time

One [`web_action`](https://docs.rual.nl/block-types/web/web_action) per intent. Test each with real data in a test document. If the new blueprint has pages with different scopes, give each action its own.

- Compare, side by side

Go through your list on both URLs. Check `view source` for the title, description and canonical, and test a form with bad input and with JavaScript off if that matters for this page.

- Move the URL over

In one sitting: give the old page another URL or deactivate its blueprint, set the web page's `event` and `l10n` to the old URLs, remove the `noindex`, save and activate. Do not leave two active pages on one URL, because which one answers is not something to rely on. The address stays the same, so links and search results keep working.

- Watch, and keep the way back open

Keep the old blueprint for a while. To go back, swap the URLs again and save: saving is what publishes. Remove the old blueprint only when you are sure.

> **State pages keep running** Nothing here forces a move. Existing state pages and UI components keep working next to web pages on the same cluster, so you can move pages when it suits you, and in any order.

## Next steps

- [Your First Page](https://docs.rual.nl/web/first-page): Get a feel for the blocks on a small page first.

- [Tutorial: A Small Webshop](https://docs.rual.nl/web/build-a-shop): Lists, actions and forms working together.

- [Actions](https://docs.rual.nl/web/actions): Where your state handlers end up.

## Frequently asked

**Are RUAL state pages being removed?**

They are deprecated, not removed: existing state pages and UI components keep running. New work should use the web blocks, and the migration guide gives a safe order to move a page across.

**What replaces state_page and state_render_page?**

web_page and web_render. Where a state page keeps state in the browser and re-renders it, a web page renders on the server and answers an action with only the parts that changed: web_foreach for lists, web_action for buttons and forms, and web_replace, web_remove, web_insert and web_update for the updates.
