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

afformative

v0.7.0

Published

A standardized way to format values in your React components.

Readme

Installation

Use one of the following commands, depending on your preferred package manager:

yarn add afformative

pnpm add afformative

npm i afformative

Quick Start

Afformative is framework-agnostic, but this section will assume usage with React.

A formatter is an object with format, stringify, and compare methods. Formatters are created using the createFormatter function.

createFormatter accepts a single object parameter. format is always required. stringify is required when the formatted output is not a string or number, since there is no safe plain-text default in that case.

import { createFormatter } from "afformative"
import { ReactNode } from "react"

const dateFormatter = createFormatter<Date, ReactNode>({
  format: value => <time dateTime={value.toISOString()}>{value.toLocaleDateString()}</time>,
  stringify: value => value.toLocaleDateString(),
  compare: value => value.valueOf(),
})

dateFormatter.format(new Date()) // <time dateTime="2026-05-30T09:17:26.263Z">30/05/2026</time>
dateFormatter.stringify(new Date()) // "30/05/2026"

Consume formatters in your UI component library through a conventional formatter prop.

import { Formatter } from "afformative"
import { ReactNode } from "react"

interface ListProps<TItem> {
  formatter: Formatter<TItem, ReactNode>
  items: TItem[]
}

const List = <TItem extends unknown>({ formatter, items }: ListProps<TItem>) => (
  <ul>
    {items.map(item => (
      <li key={formatter.stringify(item)}>{formatter.format(item)}</li>
    ))}
  </ul>
)

The stringify method is useful when you need a plain-text representation of a value. For example, a combobox component can use it to match items against the user's typed input. The default implementation of stringify is String(format(value)).

Accessing State

Create formatters inside hooks to access React context.

const useEnumFormatter = (enumType: string): Formatter<string, ReactNode> => {
  const enumTranslationKeys = useSelector(selectEnumTranslationKeys(enumType))
  const intl = useIntl()

  return useMemo(
    () =>
      createFormatter<string, ReactNode>({
        format: value => (
          <FormattedMessage defaultMessage={value} id={enumTranslationKeys[value]} />
        ),
        stringify: value =>
          intl.formatMessage({ defaultMessage: value, id: enumTranslationKeys[value] }),
      }),
    [intl, enumTranslationKeys],
  )
}

Comparing Values

Every formatter exposes a compare method that can be passed directly to Array.prototype.sort. The default implementation compares the return values of stringify using localeCompare.

In the following example, Amount objects are sorted first by currency, then by value.

import { createFormatter } from "afformative"
import { ReactNode } from "react"

interface Amount {
  currency: string
  value: number
}

const amountFormatter = createFormatter<Amount, ReactNode>({
  format: ({ currency, value }) => (
    <span className="currency">{`${currency} ${value.toFixed(2)}`}</span>
  ),
  stringify: ({ currency, value }) => `${currency} ${value.toFixed(2)}`,
  compare: (a, b) => a.currency.localeCompare(b.currency) || a.value - b.value,
})

const amounts: Amount[] = [
  { currency: "USD", value: 3 },
  { currency: "EUR", value: 1 },
  { currency: "EUR", value: 5 },
  { currency: "USD", value: 2 },
]

amounts.sort(amountFormatter.compare)
// [{ EUR, 1 }, { EUR, 5 }, { USD, 2 }, { USD, 3 }]

Formatter Context

You can pass context to all formatter methods. Consider the following table component as an example.

import { Formatter } from "afformative"
import { ReactNode } from "react"

interface TableFormatterContext {
  row: number[]
  cellIndex: number
}

interface TableProps {
  rows: number[][]
  formatter: Formatter<number, ReactNode, TableFormatterContext>
}

const Table = ({ rows, formatter }: TableProps) => (
  <table>
    {rows.map(row => (
      <tr>
        {row.map((cell, cellIndex) => (
          <td>{formatter.format(cell, { row, cellIndex })}</td>
        ))}
      </tr>
    ))}
  </table>
)

Context allows consumers of this table component to write purpose-built formatters that can take other values in the same row into account.

For example, the following formatter changes the color of the cell value based on the previous value in the same row.

import { createFormatter } from "afformative"
import { ReactNode } from "react"

import { TableFormatterContext } from "./Table"

const rowTrendFormatter = createFormatter<number, ReactNode, TableFormatterContext>({
  format: (value, { row, cellIndex } = {}) => {
    if (!cellIndex || !row) {
      return <span>{value}</span>
    }

    const previousValue = row[cellIndex - 1]

    return <span style={{ color: value >= previousValue ? "green" : "red" }}>{value}</span>
  },
})

This formatter only makes sense within the context of our table component.

Because row and cellIndex are passed as context, the formatter still receives only the cell value as its first parameter. This means generic formatters (e.g. a currency formatter) can be passed to the table component unchanged.

Formatter Meta

As explained above, context passes information from the consumer to the formatter. The meta property works in the opposite direction, passing information from the formatter to the consumer. The base FormatterMeta interface is extensible via declaration merging.

For example, formatters can explicitly mark themselves as print-friendly. Consumers can then decide to use stringify instead of format based on this flag.

declare module "afformative" {
  interface FormatterMeta<TInput, TOutput, TContext> {
    isPrintFriendly?: boolean
  }
}

const colorfulFormatter = createFormatter<string, ReactNode>({
  format: value => <Colorful>{value}</Colorful>,
  stringify: value => value,
  meta: {
    isPrintFriendly: true,
  },
})

interface PrinterProps {
  content: string
  formatter: Formatter<string, ReactNode>
}

const Printer = ({ content, formatter }: PrinterProps) => {
  return formatter.meta?.isPrintFriendly ? formatter.format(content) : formatter.stringify(content)
}

Changelog

See the CHANGELOG.md file.

License

All packages are distributed under the MIT license. See the license here.