sveltejs / sveltejs/kit

Improve `page.state` type

Open
#13,588 0 comments 3 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

types / typescript
Dominant language
JavaScript
Stars
20.8k
Forks
2.3k
Avg merge
1d 16h
Merged PRs (30d)
156

Description

Describe the problem

Currently, page.state is typed as App.PageState, which is a global type.
This does not seem the best way to type an object which may be used for shallow navigation/history snapshotting in different places, each with different requirements.

For example, in a multi-step form I may want to persist the form state in page.state via pushState so that the user can navigate back and edit previous inputs.

On another page, I may want to control which widgets are open.

Both of these scenarios may happen in the same app and a global type is not well-suited to satisfy this use-case in an scalable way.

Shallow navigation has been one of the two rough corners I have found while developing with SvelteKit (with the other being lack of proper, composable dependency injection. See https://github.com/sveltejs/svelte/discussions/15225).

While this is definitely a minor annoyance given than most of the time you would be handling simple state (say an object with 1-3 keys) I think the lack of proper typescript and composability should be addressed at some point.

Describe the proposed solution

Any of these solutions would provide a better DX working with page.state:

  1. Allow exporting a PageState type from +page.ts and +layout.ts files so we can cast page.state like the following:
<script lang="ts">
    import {page} from '$app/state';
    import type {PageState} from './$types.js';

    const state = page.state as PageState;
</script>
  1. Solution 1 + outright export page.state from './$types.js' so we can do the following:
<script lang="ts">
    import {state} from './$types.js';
</script>
  1. Rethink state: allow exporting a state function from +page.ts / +layout.ts files. Then make state available as a top level prop returned by the $props rune. The state prop is typed the same way as the data prop and replaceState is called before rendering the route so state is populated with its initial value before it is used.
//+page.ts
//State might be an async function and has access to the parent state function and its sibling load function.
//We may want to allow exporting a state type instead of function in case we don't want an initial state but still want a typed state prop
export const state = async ({parent, load})=>{
    const user_preferences = await load();
    return {
        last_video: {id: '1', time: 1.50, volume: user_preferences.volume}
    };
}
export const load = async ()=>{
    return {volume: 0.75};
}
<script lang="ts">
    import type {PageProps} from './$types.js';
    const {data, state}: PageProps = $props();
</script>

Proposals are ordered by the amount of required changes.

Proposal 2 would be the most controversial since it lacks semantic coherence (file named types is exporting data) but more ergonomic (no need to manually cast page.data).

While proposal 3 is my favorite solution since it does not only address the type issue but makes state composable, I understand that it is quite a big change for a small gain and that being able to access state through the $props rune and through the '$app/state' import is not ideal. This might be something to consider for a major release though.

The PageState type should follow the same load function return type rules: it should be shallowly merged with the parent +page.ts / +layout.ts PageState type by default and it should be available to all subroutes.

Alternatives considered

No response

Importance

nice to have

Additional Information

No response

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the current page.state typing and the proposed entry points in +page.ts, +layout.ts, './$types.js', and '$app/state'. Compare the three proposed designs and determine the intended parent-state merging and subroute behavior. Done means an agreed, documented type design with its impact on page props and generated types established.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
frontend, web-dev
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.