npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

asasvirtuais

v4.0.1

Published

A React framework for building full-stack apps where code is organized by feature, not by layer.

Downloads

268

Readme

asasvirtuais

A React framework for building full-stack apps where code is organized by feature, not by layer.


Primitives

Three building blocks, each usable on its own.

FieldsProvider — field state

import { FieldsProvider } from 'asasvirtuais/fields'

<FieldsProvider defaults={{ title: '', done: false }}>
  {({ fields, setField }) => (
    <div>
      <input value={fields.title} onChange={e => setField('title', e.target.value)} />
      <input type="checkbox" checked={fields.done} onChange={e => setField('done', e.target.checked)} />
    </div>
  )}
</FieldsProvider>

ActionProvider — async action state

import { ActionProvider } from 'asasvirtuais/action'

<ActionProvider params={{ id: todo.id }} action={archiveTodo} onResult={() => router.push('/')}>
  {({ submit, loading, error }) => (
    <button onClick={submit} disabled={loading}>
      {loading ? 'Archiving...' : 'Archive'}
    </button>
  )}
</ActionProvider>

Form — fields + action together

import { Form } from 'asasvirtuais/form'

<Form defaults={{ email: '', password: '' }} action={login} onResult={handleResult}>
  {({ fields, setField, submit, loading, error }) => (
    <form onSubmit={submit}>
      <input value={fields.email} onChange={e => setField('email', e.target.value)} />
      <input type="password" value={fields.password} onChange={e => setField('password', e.target.value)} />
      <button type="submit" disabled={loading}>Login</button>
      {error && <p>{error.message}</p>}
    </form>
  )}
</Form>

Full-stack CRUD

The framework provides a schema-first CRUD layer where create, update, and remove operations automatically keep the UI in sync through a reactive index — no manual state updates, no refetching.

Project structure

app/
├── schema.ts             # All table schemas in one place
├── actions.ts            # Server actions — the backend
├── providers.tsx         # App-level providers
├── layout.tsx
├── todos/
│   ├── schema.ts          # Schema + types
│   ├── fields.tsx        # Input components
│   ├── forms.tsx         # Create / Update / Delete / Filter forms
│   ├── components.tsx    # Display components
│   └── provider.tsx      # TableProvider + hook

1. Schema

Each model defines readable (what comes out of the database) and writable (what users can create or modify):

// app/todos/schema.ts
import z from 'zod'

export const readable = z.object({
  id: z.string(),
  title: z.string(),
  done: z.boolean(),
  author: z.string(),
  createdAt: z.string(),
})

export const writable = readable.pick({
  title: true,
  done: true,
})

export const schema = { readable, writable }

export type Readable = z.infer<typeof readable>
export type Writable = z.infer<typeof writable>

All models are assembled into a single database schema file:

// app/schema.ts
import { schema as todosSchema } from './todos/schema'
import { schema as tagsSchema } from './tags/schema'

export const schema = {
  todos: todosSchema,
  tags: tagsSchema,
}

2. Server actions — the backend

The backend is plain Next.js server actions. You pass them directly to the provider — no REST routes, no fetch client needed:

// app/actions.ts
'use server'

import { firestoreInterface } from 'asasvirtuais-firebase/interface'
import { auth0 } from '@/lib/auth0'

const db = firestoreInterface()

function clean(obj: any): any {
  if (Array.isArray(obj)) return obj.map(clean)
  if (obj !== null && typeof obj === 'object' && obj.constructor === Object) {
    return Object.fromEntries(
      Object.entries(obj)
        .filter(([_, v]) => v !== undefined)
        .map(([k, v]) => [k, clean(v)])
    )
  }
  return obj
}

export const find = async (props: any) => db.find(props)

export const list = async (props: any) => db.list(props)

export const create = async (props: any) => {
  const session = await auth0.getSession()
  if (!session?.user) throw new Error('Unauthorized')

  const data = clean({ ...props.data })

  if (props.table === 'todos') {
    data.author = session.user.id
    data.done = false
    data.createdAt = new Date().toISOString()
  }

  return db.create({ ...props, data })
}

export const update = async (props: any) => {
  const session = await auth0.getSession()
  if (!session?.user) throw new Error('Unauthorized')

  const data = clean({ ...props.data })

  if (props.table === 'todos') {
    const existing = await db.find({ table: 'todos', id: props.id })
    if (existing.author !== session.user.id) throw new Error('Forbidden')
  }

  return db.update({ ...props, data })
}

export const remove = async (props: any) => {
  const session = await auth0.getSession()
  if (!session?.user) throw new Error('Unauthorized')

  if (props.table === 'todos') {
    const existing = await db.find({ table: 'todos', id: props.id })
    if (existing.author !== session.user.id) throw new Error('Forbidden')
  }

  return db.remove(props)
}

This is where business logic lives: auth, default values, permission checks. All in one place, all readable top to bottom.


3. Providers

// app/providers.tsx
import { InterfaceProvider } from 'asasvirtuais/interface-provider'
import { DatabaseProvider } from 'asasvirtuais/react-interface'
import { TodosProvider } from '@/app/todos/provider'
import * as db from '@/app/actions'

export default function AppProviders({ children }: { children: React.ReactNode }) {
  return (
    <InterfaceProvider
      find={db.find}
      list={db.list}
      create={db.create}
      update={db.update}
      remove={db.remove}
    >
      <DatabaseProvider>
        <TodosProvider>
          {children}
        </TodosProvider>
      </DatabaseProvider>
    </InterfaceProvider>
  )
}
// app/layout.tsx
import AppProviders from './providers'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <AppProviders>
          {children}
        </AppProviders>
      </body>
    </html>
  )
}

4. Model provider

// app/todos/provider.tsx
'use client'
import { TableProvider, useTable } from 'asasvirtuais/react-interface'
import { useInterface } from 'asasvirtuais/interface-provider'
import { schema } from '.'

export function useTodos() {
  return useTable('todos', schema)
}

export function TodosProvider({ children }: { children: React.ReactNode }) {
  return (
    <TableProvider table="todos" schema={schema} interface={useInterface()}>
      {children}
    </TableProvider>
  )
}

5. UI

// app/todos/page.tsx
'use client'
import { useEffect } from 'react'
import { useTodos } from './provider'
import { SingleProvider } from 'asasvirtuais/react-interface'
import { schema } from '.'
import { TodoItem } from './components'
import { CreateTodo } from './forms'

export default function TodosPage() {
  const { array, list } = useTodos()

  useEffect(() => { list.trigger({}) }, [])

  return (
    <div>
      <CreateTodo />
      {array.map(todo => (
        <SingleProvider key={todo.id} id={todo.id} table="todos" schema={schema}>
          <TodoItem />
        </SingleProvider>
      ))}
    </div>
  )
}

When create resolves, the item appears in array immediately. Same for update and remove.


Listing vs. filtering

useTable().list — reactive, global

Use this when you want all records in the reactive index. Results live in array and stay in sync with every create, update, and remove automatically:

const { array, list } = useTodos()

useEffect(() => { list.trigger({}) }, [])

// array updates automatically when any todo is created, updated, or removed
return array.map(todo => (
  <SingleProvider key={todo.id} id={todo.id} table="todos" schema={schema}>
    <TodoItem />
  </SingleProvider>
))

FilterForm — local, paginated, or conditional

Use FilterForm when you need pagination, live search, or results that belong to the component rather than the global index. Results live in form.result and only update when submit is called:

import { FilterForm } from 'asasvirtuais/form'
import { schema } from '.'

<FilterForm table="todos" schema={schema} defaults={{ query: { done: false } }} autoTrigger>
  {({ result, loading, fields, setField, submit }) => (
    <div>
      <input
        placeholder="Search..."
        value={fields.query?.title ?? ''}
        onChange={e => {
          setField('query', { title: e.target.value })
          submit()
        }}
      />
      {loading && <p>Loading...</p>}
      {result?.map(todo => <p key={todo.id}>{todo.title}</p>)}
    </div>
  )}
</FilterForm>

Async selector fields

When a form needs the user to pick a record from another table, FilterForm composes naturally inside a field component. The field reads and writes to the parent form's context via useFields() — no props needed to bridge them.

Say a todo can be tagged, and the user needs to search and select a tag while creating the todo:

// app/todos/fields.tsx
import { useFields } from 'asasvirtuais/fields'
import { FilterForm } from 'asasvirtuais/form'
import { schema as tagsSchema } from '@/app/tags'

export function TagSelectorField() {
  // reads/writes to whatever Form or FieldsProvider this is rendered inside
  const { fields, setField } = useFields<{ tagId: string }>()

  return (
    <FilterForm table="tags" schema={tagsSchema} defaults={{ query: {} }}>
      {({ fields: search, setField: setSearch, submit, result }) => (
        <div>
          <input
            placeholder="Search tags..."
            onChange={e => {
              setSearch('query', { name: e.target.value })
              submit()
            }}
          />
          <ul>
            {result?.map(tag => (
              <li
                key={tag.id}
                onClick={() => setField('tagId', tag.id)}
                style={{ fontWeight: fields.tagId === tag.id ? 'bold' : 'normal' }}
              >
                {tag.name}
              </li>
            ))}
          </ul>
        </div>
      )}
    </FilterForm>
  )
}

Use it inside any form — it just works:

// app/todos/forms.tsx
import { CreateForm } from 'asasvirtuais/form'
import { schema } from '.'
import { TitleField, TagSelectorField } from './fields'

export function CreateTodo({ onSuccess }: { onSuccess?: () => void }) {
  return (
    <CreateForm table="todos" schema={schema} defaults={{ title: '', tagId: '' }} onSuccess={onSuccess}>
      {({ submit, loading }) => (
        <div>
          <TitleField />
          <TagSelectorField />
          <button onClick={submit} disabled={loading}>
            {loading ? 'Creating...' : 'Create Todo'}
          </button>
        </div>
      )}
    </CreateForm>
  )
}

The FilterForm queries the tags table asynchronously. The CreateForm owns the selected tagId. Neither knows about the other.


The single record pattern

SingleProvider makes a record available to all its descendants without prop drilling. When multiple components share one record, wrap them all in one provider:

import { SingleProvider, useSingle } from 'asasvirtuais/react-interface'

// Detail page
<SingleProvider id={params.id} table="todos" schema={schema}>
  <TodoDetail />
  <UpdateTodoForm />
  <DeleteTodoButton />
</SingleProvider>

// Inside any of those:
function TodoDetail() {
  const { single } = useSingle(schema, 'todos')
  return <h1>{single.title}</h1>
}

If the record isn't in the reactive index yet, SingleProvider fetches it automatically.


Effects

There is no middleware or lifecycle configuration. Effects are code written around the action:

// Before submit
<button onClick={() => {
  validateForm(form.fields)
  form.submit()
}}>
  Save
</button>

// After success
<CreateForm
  table="todos"
  schema={schema}
  onSuccess={todo => {
    router.push(`/todos/${todo.id}`)
    showNotification('Todo created!')
  }}
>
  {/* ... */}
</CreateForm>

// Using field values without submitting
<button onClick={() => saveDraftLocally(form.fields)}>
  Save Draft
</button>

Naming pattern examples

| Concept | Pattern | Example | |---|---|---| | Table name | lowercase plural | 'todos' | | Schema types | Readable, Writable | type Readable = z.infer<...> | | Field components | {Field}Field | TitleField, DoneField | | Provider | {Model}sProvider | TodosProvider | | Hook | use{Model}s() | useTodos() | | Create form | Create{Model} | CreateTodo | | Update form | Update{Model} | UpdateTodo | | Delete action | Delete{Model} | DeleteTodo | | Item component | {Model}Item | TodoItem | | Detail component | Single{Model} | SingleTodo |