Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime
  1. Managing state
  2. Pagination placeholders

Pagination placeholders#

insertPaginationPlaceholderData keeps the previous page on screen while the next one loads, so paging through a list never flashes an empty state.

Use it when a query is paginated with an identifier per page. Not when you just want to avoid a flicker on a non-paginated query — a query already keeps its previous value while loading, with no configuration (query).

typescript
import { insertPaginationPlaceholderData } from '@craft-ts/core';

The common case#

It is a higher-order insertion: call it with a config and pass the result to query. config.initialValue is both the default value and the page type — which is why currentPageData is a Signal<T> that is never undefined.

typescript
const pagination = yield* state('pagination', 1);

const { userQuery } = yield* query(
  'userQuery',
  {
    params: pagination,
    identifier: (params) => '' + params,
    loader: function* ({ params }) {
      const response = yield* CraftHttpClient.get(({ response }) => ({
        url: `/api/users?page=${params}`,
        success: response<User[]>(),
      }));
      return response.json();
    },
  },
  insertPaginationPlaceholderData({ initialValue: [] as User[] }),
);

// Access the current page data (or placeholder data during loading)
const data = userQuery.currentPageData();

// Check the loading status of the current page
const status = userQuery.currentPageStatus();

// Determine if placeholder data is being shown
const isPlaceholder = userQuery.isPlaceHolderData();

// Get the current page identifier
const identifier = userQuery.currentIdentifier();

Returned Properties#

PropertyTypeDescription
currentPageDataSignal<T>The data for the current page, or placeholder data from the previous page during loading. Falls back to initialValue (never undefined).
currentPageStatusSignal<ResourceStatus>The loading status of the current page ('idle', 'loading', 'resolved', 'error')
isPlaceHolderDataSignal<boolean>true when showing previous page data as a placeholder
currentIdentifierSignal<string>The identifier of the current page

Custom Outputs (build callback)#

Pass an optional second argument to attach your own computed values or methods next to the pagination outputs. Its helpers (state, set, update, patch) are scoped to the current page (the displayed data), so mutations only affect the page the user is looking at — other cached pages are left untouched.

typescript
const { usersQuery } = query(
  'usersQuery',
  {
    params: pagination,
    identifier: (params) => `${params.page}-${params.pageSize}`,
    loader: function* ({ params }) {
      return yield* ApiService.getDataList(params);
    },
  },
  insertPaginationPlaceholderData(
    { initialValue: [] as Data[] },
    ({ state, settledState, set }) => ({
      // a computed derived from the current page
      totalOfUnCompletedData: craftUse(craftComputed('totalOfUnCompletedData', function* () {
        return (yield* state()).filter((d) => !d.completed).length;
      })),
      settledCount: craftUse(craftComputed('settledCount', function* () {
        return (yield* settledState()).length;
      })),
      markAsCompleted: function* (id: string) {
        const current = yield* state();
        return yield* set(
          current.map((d) => (d.id === id ? { ...d, completed: true } : d)),
        );
      },
    }),
  ),
);

yield* usersQuery.totalOfUnCompletedData(); // number
yield* usersQuery.markAsCompleted('42');

The build context exposes:

HelperTypeDescription
stateyieldable reader for TThe current page data (or initialValue)
settledStategenerator reader for TThe current page data only when loaded; suspends during the first load or a page transition
setyieldable write returning TReplace the current page data (no-op if not loaded)
updateyieldable write returning TUpdate the current page data from its previous value
patchyieldable write returning TPatch the current page data with a partial value

The pagination outputs (currentPageData, currentPageStatus, isPlaceHolderData, currentIdentifier) are also available in the build context.

A full paginated component
typescript
import {
  button,
  craftComponent,
  div,
  forNode,
  ifNode,
  span,
} from '@craft-ts/component';
import { craftComputed, craftUse, query, state } from '@craft-ts/core';

export const UsersList = craftComponent('UsersList', {}, function* () {
  const page = yield* state('page', 1, ({ state, update, set }) => ({
    next: () => update((value) => value + 1),
    previous: function* () {
      const current = yield* state();
      return yield* set(Math.max(1, current - 1));
    },
    isFirst: craftUse(
      craftComputed('isFirst', function* () {
        return (yield* state()) === 1;
      }),
    ),
    label: craftUse(
      craftComputed('label', function* () {
        return `Page ${yield* state()}`;
      }),
    ),
  }));

  const userQuery = yield* query(
    'userQuery',
    {
      params: page,
      identifier: (page) => `page-${page}`,
      loader: async ({ params }) =>
        (await fetch(`/api/users?page=${params}`)).json() as Promise<User[]>,
    },
    insertPaginationPlaceholderData({ initialValue: [] as User[] }),
  );

  return [
    div(
      {
        class: function* () {
          return (yield* userQuery.isPlaceHolderData())
            ? 'users-list loading'
            : 'users-list';
        },
      },
      forNode(userQuery.currentPageData, { track: (user) => user.id }, (user) =>
        UserCard({ user }),
      ),
    ),

    // `pagerSheet` is the page's sheet, from users-page.style.ts.
    div({ class: pagerSheet.bar }, [
      button({ click: page.previous, disabled: page.isFirst }, 'Previous'),
      span(page.label),
      button({ click: page.next }, 'Next'),
    ]),

    ifNode(userQuery.isPlaceHolderData, () =>
      div({ class: pagerSheet.loading }, 'Loading new page…'),
    ),
  ];
});

How it works#

  1. When the page parameters change, the insertion checks whether the new page's data is already cached.

  2. If the new page is loading and has no data yet, it serves the previous page's data as a placeholder.

  3. isPlaceHolderData tells you that is what is on screen — use it to dim the list or show a spinner.

  4. Once the real data arrives, it switches over automatically.

Pitfalls#

It needs an identifier. Without one page identity, there is no "previous page" to fall back to.

initialValue defines the page type. Passing [] untyped collapses currentPageData to never[] — write [] as User[].

Mutating through the build helpers only affects the current page. Other cached pages are untouched, which is usually what you want, but means a global change needs a reload.

See Also#

  • query — the base primitive

  • Reacting to mutations

  • Insertions

←GlobalPersisterHandlerSchema validation→

On this page

  • The common case
  • Returned Properties
  • Custom Outputs (build callback)
  • How it works
  • Pitfalls
  • See Also

@craft-ts · MIT licensed

GitHubnpm

Search