F is a javascript front-end framework just like React with same hooks. But by using HTML custom elements (web components) instead of JSX it doesn't need a build step and is future proof.
There are also signals hooks that don't exist on the React world. I recommend using them but pick the style you prefer.
I didn't want to learn new javascript front-end frameworks.
There are alternatives that use custom elements but code is so big! F implementation is short and easy to understand. Well, it was short until signals support was added.
npm i thenameisf
This package ships a Claude Code skill at skills/thenameisf/ that teaches the agent how to write idiomatic thenameisf components. To make it available in a project that depends on thenameisf, symlink it into that project's .claude/skills/:
mkdir -p .claude/skills
ln -s ../../node_modules/thenameisf/skills/thenameisf .claude/skills/thenameisfCommit the symlink and the whole team picks it up. Upgrading thenameisf upgrades the skill automatically.
There are two ways, functional and class-based.
Let's consider this html for both examples:
<!doctype html>
<html>
<head>
<script defer src="my-app.js"></script>
</head>
<body>
<my-app></my-app>
<body>
</html>import { f, useState, useEffect } from 'thenameisf'
// this automatically defines "my-app" custom element (used on above HTML)
f(function myApp ({ html }) {
const [test, setTest] = useState(1)
useEffect(() => {
const i = setInterval(() => {
setTest(t => ++t)
}, 2000)
return () => clearInterval(i)
}, [])
return html`<my-child props=${{ test }} />`
})
// this automatically defines "my-child" custom element
f(function myChild ({ html, h, props }) {
// html, h, this.html and this.h are the same
// there's also svg, s, this.svg and this.s
return this.h`<div>This is a test: ${props.test}<div>`
})Note that custom elements require atleast two words for the tag name, that is, "myApp" becomes "my-app", while "app" wouldn't work.
import { F, useState, useCallback } from 'thenameisf'
customElements.define('my-app', class extends F {
// you can use all custom element methods
connectedCallback () {() {
super.connectedCallback()
console.log('connected')
}
component () {
const [test, setTest] = useState(1)
const onClick = useCallback(() => setTest(t => ++t), [setTest])
return this.html`<ul onclick=${onClick}>
${[...Array(test).keys()].map(i => this.html({ key: i })`<li>${++i}</li>`)}
</div>`
}
})- useCallback
- useEffect
- useInsertionEffect
- useMemo
- useRef
- useState
The useGlobalState hook is like useState but shares
the state across all components if using same namespace.
The first component to call useGlobalState with a specific namespace sets the initial value.
// on my-component-a.js
const [test, setTest] = useGlobalState('a-namespace', 1)
setTest(20)
// on my-component-b.js
// test === 20
const [test, setTest] = useGlobalState('a-namespace', 1)The React's useContext hook equivalent is the useClosestState
one that is like useState but shares the state from the closest
ancestor component that called useClosestState with same namespace
and a initial value.
// on my-component-grand-grand-parent.js
const [test, setTest] = useClosestState('a-namespace', 5)
setTest(20)
// on my-component-grand-parent.js
const [test, setTest] = useClosestState('a-namespace', 1)
// on my-component.js
// test === 1
const [test, setTest] = useClosestState('a-namespace')You can use the function from useState directly as an event callback
without resorting to useCallback.
const [value, setValue] = useState('')
return html`<input value={value} oninput={setValue} />`When adding/removing an item from a useState/useGlobalState's array state,
there is no need to recreate the array to detect a change. Great for
infinite loading.
const [array, setArray] = useState([0])
const onClick = useCallback(() => setArray(a => {
// instead of return [...a, a.length]
a.push(a.length); return a
}), [setArray])
return html`<div onclick=${onClick}>Array length: ${array.length}</div>`Updating an existing array item would need a new array, though. Unless you
add the eqKey symbol as key. F will check if its value has changed to
determine if the array has changed (it can also be used for other object types).
// or import { eqKey } from 'thenameisf'
const onClick = useCallback(e => {
setArray((a, eqKey) => {
a[e.target.dataset.index] = 'new value'
a[eqKey] = Math.random()
return a // instead of returning [...a]
})
}, [setArray])Signals are an alternative reactive programming paradigm to React's useState.
It is more performant because when a signal value changes, just components
that use the value will re-render, not the component that instantiated the signal.
With useState, all the tree of components starting from the useState caller
would re-render.
useSignal returns an object with .get() and .set() methods
similar to useState returned array's items. It also replaces
useRef. If you call .get(false), it won't cause re-renders.
We adopted a $ suffix as a convention for signal variable names
so that you know that the variable has a getter and a setter.
In the example below, clicks on the parent component's button won't re-render the parent, just the child.
f(function componentA ({ html }) {
const count$ = useSignal(0) // or useSignal(() => 0) for lazy initial value
return html`<div>
<button onclick=${() => count$.set(v => ++v)}
<component-b props=${{ count$ }} />
</div>`
})
f(function componentB ({ html, props }) {
return html`<span>${props.count$.get()}</span>`
})Tip
You can use example$() and example$(newValue) call style instead of the more verbose example$.get() and example$.set(newValue).
If you just want to read the current value without subscribing to updates,
use example$.peek(). It is an alias for example$.get(false).
It is useful inside contexts that subscribe by default, such as useComputed,
useAsyncComputed, the callback passed to useTask's track, or directly
inside a component's render body.
f(function counter ({ html }) {
const count$ = useSignal(0)
const delta$ = useSignal(1)
// re-runs only when count$ changes, not when delta$ changes,
// because delta$ is read with .peek()
const sum$ = useComputed(() => count$.get() + delta$.peek())
return html`<div>${sum$.get()}</div>`
})Note that outside of subscribing contexts (e.g. in event handlers or
directly in a useTask body but outside its track callback), reads
don't subscribe anyway, so .peek() and .get() behave identically -
.peek() is just a more explicit way to signal the intent.
It also works on computeds and on signals/computeds created through useStore.
The eqKey may be used here too if you don't want to return a new object
on signal update. For example:
// or import { eqKey } from 'thenameisf'
signal$.set((prevValue, eqKey) => {
prevValue.newItem = 3
prevValue[eqKey] = Math.random()
return prevValue
// instead of return { ...prevValue, newItem: 3 }
})Drop-in replacement to useMemo. Difference is it doesn't need
to list dependencies. It isn't compatible with useState values
(can't track their updates).
It is used like a regular signal but it doesn't have a .set() setter.
Nevertheless, we adopted the same $ suffix because the difference
isn't big enough to justify using another one.
When a signal (or other computation) inside it changes,
it schedules a refresh of its cached computed value for when
its read somewhere with .get().
// the function argument can't be async
const doubleCount$ = useComputed(() => count$.get() * 2)
console.log(doubleCount$.get())An easy way to group the creation of signals and computations. It looks at the suffixes of the object argument keys to know what to create.
const childProps = useStore({
// signal, because it ends with $
count$: 0
// computed, because it ends with $ and is initialized with a function
doubleCount$ () { return this.count$.get() * 2 },
notReactive: 'Things won\'t re-render if I change',
deep: {
// signal
example$: 3
},
aSignal$: { anotherSignal$: 'signal\'s signal' }
})Pass a function to useStore to run just once any heavy computations required to initialize its values.
const childProps = useStore(() => {
const count$ = heavyComputation() // can't return a function
const doubleCount$ = function () { return this.count$.get() * 2 }
const notReactive = 'Things won\'t re-render if I change'
const deep = { example$: 3 }
return {
count$,
// can't initialize a signal to a function value
// because functions on fields with keys ending with $
// will be used to initialize computations
signalFunction$: undefined
doubleCount$, // computed
notReactive,
deep
}
})
// a store's signal field may be set to a function after the store initialization
useTask(() => {
signalFunction$.set(function example () { /* ... */ })
})Alternatively, its possible to lazy init a store's signal by passing a function with a strategy="signal" property:
const store = useStore({
lazySignalExample$: (() => {
// "heavyComputation" call may even return a function
const fn = () => heavyComputation()
fn.strategy = 'signal'
return fn
})()
})Same thing as useStore but the first argument is a string
that you can reference on other components. It's like
useGlobalState but for stores.
// on my-component-a.js
const store = useGlobalStore('example123', { /* ... */ })
// on my-component-b.js
const sameStore = useGlobalStore('example123')Same thing as useGlobalStore but for storing a single signal.
const example$ = useGlobalSignal('<namespace>', <initial value>)Same thing as useGlobalStore but for storing a single computation.
const example$ = useGlobalComputed('<namespace>', <function>)This is closer to React's useContext behavior than useGlobalStore.
Gets the store from the closest ancestor that initialized
a store with a specific namespace using useClosestStore
with a second argument.
// on grandparent component
const store = useClosestStore('example123', { a$: 1 })
// on parent component
const store2 = useClosestStore('example123', { b$: 2 })
// on descendant component
const { b$ /* there's no a$ */ } = useClosestStore('example123')Same thing as useClosestStore but for storing a single signal.
// on ancestor component
const example$ = useClosestSignal('<namespace>', <initial value>)
// on descendant component
const example2$ = useClosestSignal('<namespace>')Same thing as useClosestStore but for storing a single computation.
// on ancestor component
const example$ = useClosestComputed('<namespace>', <function>)
// on descendant component
const example2$ = useClosestComputed('<namespace>')Drop-in replacement to useEffect and useInsertionEffect.
Use the track function instead of the dependencies array.
It can't track values from useState.
Use the cleanup function instead of returning a function.
Signature: useTask(fn, config)
Config argument (listed values are the default ones):
{
// or 'visible' which would run just when component is visible
when: 'init',
// these are used for "when=visible" and work the same as the
// options for the IntersectionObserver api
root: null, rootMargin: '50%', threshold: 0
// or 'rendering' which would run after the component html's first render
// and it's good for waiting for refs set on components like <div ref=${signal$}>
// to be available
after: 'insertion',
// or 'serial' which queues tasks sequentially
execution: 'concurrent',
// used for serial execution
queueSize: 1
}Example:
useTask(({ track, cleanup }) => {
const value = track(() => aSignal$.get())
const i = setInterval(() => {
console.log(value)
}, 2000)
cleanup(() => clearInterval(i))
})Like useComputed, it has only a getter. The arguments and usage are identical to useTask, except that it expects you to return a value.
const computed$ = useAsyncComputed(async ({ track, cleanup }) => {
track(() => aSignal$.get())
const c = new AbortController()
cleanup(() => c.abort())
const person = await fetchPerson(c.signal)
return person.name
})
console.log(computed$.get()) // starts undefinedYou may initialize it with a value, or lazily with a function, before running the function body:
const computed$ = useAsyncComputed('Unknown Person', async () => { /* ... */ return 'Alice' })The returned computation variable has a .promise$ signal property that is set
to an object as follows:
{
status: 'resolved' // pending / rejected
hasValue: true, // false
isLoading: true, // false
error: null // thrown error
}It may be paired with the accompanying component `:
import { f } from 'thenameisf'
import 'f/components/f-async-computed.js'
f(function myComponent ({ html }) {
const asyncComputedExample$ = useAsyncComputed(async () => {
const user = await fetchUser()
return user
})
return html`
<f-async-computed
props=${{
asyncComputed$: asyncComputedExample,
onPending: () => this.h`Loading...`,
onRejected: error => this.h`Error: ${error.message}`,
onResolved: result => this.h`Success: ${result}`
}}
>
<div>Can use it directly too: ${asyncComputedExample$.get() ?? 'not loaded yet'}</div>
`
})You probably won't need this because React's useCallback can already be used with signals/computeds.
const onClick = useCallback(() => signalB$.set(signalA$.get() + 1))It works the same except that all calls to signals/computeds' .get()s
are treated as if they were called as .get(false).
It could be useful if you don't control the function argument.
// same as const thisDoesntCauseRerenders = useCallback(() => console.log(signalA$.get(false)))
const thisDoesntCauseRerenders = useUntrackedCallback(() => console.log(signalA$.get()))
thisDoesntCauseRerenders()This is just if your fellow dev is using React paradigm while you are using signals. You use it to turn a useState or useMemo (or non-signal prop) into a signal.
const [state, setState] = useState(0)
const signal$ = useStateSignal(state, setState)
const memo = useMemo(() => state * 2, [state])
const computed$ = useStateSignal(memo)
const computed2$ = useStateSignal(props.example)Instead of "useStateSignal", you may use the "" component to inline-convert some props, like when you are mapping an array to components. This way you can easily pass all props you want as signals to be consistent.
import { f } from 'thenameisf'
import 'f/components/f-to-signals.js'
f(function myComponent ({ h }) {
const users$ = useSignal(['Alice', 'Bob'])
const test = 'Hi!'
return h`${users$.get().map(user => h({ key: user })`
<f-to-signals props=${{
from: ['user'], // the user field will be turned into a user$ signal prop
user,
test,
render: ({ html, props }) => { return html`<div>Hello ${props.user$()} - ${props.test}</div>` }
}} />`
`)}`
})The inverse, to turn a signal into a state or a computed into a memo.
const signal$ = useSignal(3)
const [state /* 3 */, setState] = useSignalState(signal$)
const computed$ = useComputed(() => signal$.get() * 2)
const memo = useSignalState(computed$) // 6Shadow-DOM components (useShadowDOM: true) use the native <slot> element.
Light-DOM components (the default) use <f-slot>, an f-provided custom
element that reads slot content from a special children prop on its host.
Import once, anywhere it's needed:
import 'thenameisf/components/f-slot.js'The parent passes content via props.children. The child uses a bare
<f-slot></f-slot> placeholder.
f(function myApp ({ html }) {
return html`
<my-card props=${{ children: html`<p>Hello world</p>` }} />
`
})
f(function myCard () {
return this.h`
<article>
<f-slot></f-slot>
</article>
`
})For multiple slots, pass a children object keyed by slot name. The bare
<f-slot></f-slot> reads children.default. A bare html`...` value
(without the object wrapper) still works for the default-only case - see
above.
The slot name can be set via the HTML name attribute or via name/name$
props (signal wins over plain prop, which wins over attribute):
f(function myApp ({ html }) {
return html`
<my-layout props=${{
children: {
header: html`<h1>Title</h1>`,
default: html`<p>Body copy</p>`,
footer: html`<small>© 2026</small>`
}
}} />
`
})
f(function myLayout () {
return this.h`
<header><f-slot name="header"></f-slot></header>
<main><f-slot></f-slot></main>
<footer><f-slot name="footer"></f-slot></footer>
`
})Using a signal for the name lets you swap which slot an <f-slot> reads at
runtime:
const which$ = useSignal('header')
this.h`<f-slot props=${{ name$: which$ }}></f-slot>`Static - whatever you put inside <f-slot> in the child's template is
captured once on first mount and rendered when the slot is missing or nullish.
No ${...} interpolation.
this.h`<f-slot name="header">Untitled</f-slot>`Reactive - for dynamic fallback, pass a fallback (or fallback$ for a
signal) prop on the <f-slot> element itself:
f(function myCard () {
const count$ = useSignal(0)
return this.h`
<f-slot props=${{ fallback: this.h`<p>Nothing slotted yet</p>` }}></f-slot>
<f-slot name="counter" props=${{ fallback$: useComputed(() => this.h`<span>Count: ${count$.get()}</span>`) }}></f-slot>
`
})The prop-based fallback is checked before the static one, so both can coexist
- the prop wins when present.
<f-slot> is itself an f component, so anything it reads with .get()
auto-subscribes its own observer. That means slot content can be a signal at
any level - and only the affected slot re-renders when the signal fires; the
host doesn't.
f(function myApp () {
const children$ = useSignal(this.h`<p>v1</p>`)
useEffect(() => { setTimeout(() => children$.set(this.h`<p>v2</p>`), 1000) }, [])
return this.h`<my-card props=${{ children$ }} />`
})<f-slot> inside myCard reads props.children$ (note the $), .get()s
it, and re-renders when the signal fires. myCard itself does not re-render.
Use a $ suffix on individual keys. <f-slot name="header"> resolves
children.header first, then children.header$. Plain wins so you can
override a signal slot without touching the parent.
f(function myApp () {
const header$ = useComputed(() => `Hello, ${user$.get()}`)
const [defaultBodyExtra, setDefaultBodyExtra] = useState('Extra')
return this.h`
<my-layout props=${{
children: {
header$, // signal
default: this.h`<p>Body - ${defaultBodyExtra}</p>`, // React-like
footer: this.h`<small>© 2026</small>` // static
}
}} />
`
})This is supported. <f-slot> first .get()s the outer signal (subscribing
to it), then reads the resolved key - calling .get() again if that key is
itself a signal (subscribing to that one too). When the outer signal fires
with a new object whose header$ is a different signal instance, f-slot
re-runs, drops its subscription to the old header$, and subscribes to the
new one. The old signal can fire at most once more before its observer set
is emptied - a wasted render, never an incorrect one.
const children$ = useSignal({
header$: useSignal('Hi'),
default: this.h`<p>Body</p>`
})F uses uhtml as its underlying
HTML rendering engine - that's what powers the html`...` tag you see in
every example. When a parent template includes <my-card><p>Hi</p></my-card>,
uhtml inserts <p>Hi</p> as real DOM children of <my-card> before
<my-card> gets a chance to render its own template; the child's render
would then clobber them. The explicit children prop sidesteps that race
and integrates with the existing prop-diffing path
(components/component.js). For shadow-DOM
components, keep using native <slot> - set useShadowDOM: true.
Client-side routing combines the useLocation hook with the <f-route>
component. Both work on top of any url-router-compatible router
instance, so install that alongside f:
npm i url-router
Create a router with path-to-handler mapping, then call useLocation(router)
inside the component that owns the route. Each handler provides a tag
(the custom element name to render) and a loadModule function used for
code splitting.
import Router from 'url-router'
import { f, useLocation } from 'thenameisf'
import 'thenameisf/components/f-route.js'
const router = new Router({
'/': { path: '/', tag: 'my-home', loadModule: () => import('./views/home.js') },
'/about': { path: '/about', tag: 'my-about', loadModule: () => import('./views/about.js') },
// params become available via loc.route$().params.npub
'/:npub(npub1.*)': { path: '/:npub(npub1.*)', tag: 'my-profile', loadModule: () => import('./views/profile.js') },
'/(.*)': { path: '/(.*)', tag: 'my-not-found', loadModule: () => import('./views/not-found.js') }
})
f(function myApp ({ html }) {
useLocation(router)
return html`
<f-route props=${{ path: '/' }} />
<f-route props=${{ path: '/about' }} />
<f-route props=${{ path: '/:npub(npub1.*)' }} />
<f-route props=${{ path: '/(.*)', shouldPreload: true }} />
`
})The path prop on <f-route> must match a key from the router definition.
Use paths (array) when a single component should mount for multiple paths —
the router entries on those paths are expected to share the same tag and
loadModule:
// in the router, both '/help' and '/faq' point to the same handler
<f-route props=${{ paths: ['/help', '/faq'] }} />Any descendant component may call useLocation() (with no argument) to get
the closest location store:
f(function myPage ({ html }) {
const loc = useLocation()
const route = loc.route$()
// route.url — URL object with pathname, searchParams, etc.
// route.params — matched route params (e.g. { npub: 'npub1abc...' })
// route.state — history.state contents
// route.handler — matched router entry ({ path, tag, loadModule })
// route.uid — monotonic id used by <f-route> for stacking/distance
return html`<h1>Pathname: ${route.url.pathname}</h1>`
})Navigation helpers on loc:
loc.pushState(state, '', '/next') // new history entry
loc.replaceState(state, '', '/same') // in-place update
loc.back()
loc.forward()
loc.go(-2)
loc.getRouterMatch('/some/path') // => { handler, params } | nullA component loaded by a parent <f-route> may itself call
useLocation(subRouter) to scope routing to its sub-tree. Descendants of
that component see the nested router via useLocation(); descendants of the
outer router (outside the sub-tree) still see the outer one.
// loaded for '/(.*)' by the outer router
import Router from 'url-router'
import { f, useLocation } from 'thenameisf'
import 'thenameisf/components/f-route.js'
const router = new Router({
'/upload': { path: '/upload', tag: 'home-upload', loadModule: () => import('./upload.js') },
'/(.*)': { path: '/(.*)', tag: 'home-index', loadModule: () => import('./index.js') }
})
f(function homeRouter () {
useLocation(router)
return this.h`
<f-route props=${{ path: '/(.*)' }} />
<f-route props=${{ path: '/upload' }} />
`
})path/path$— path key from the router to match.paths/paths$— array form, when one component should mount for multiple paths.shouldPreload/shouldPreload$— defaultfalse. Whentrue, the module is imported and rendered as soon as the app starts, regardless of the current path. Useful for the fallback/(.*)route so it's ready instantly.maxVisibleDistance/maxVisibleDistance$— default0. Keeps the previous route visible while the user navigates through nearby history entries. Useful for overlay patterns where a previous view shouldn't unmount immediately.shouldUpdateUidWhenPathMatches/shouldUpdateUidWhenPathMatches$— defaulttrue. Controls whether repeated matches re-bump the stored uid used bymaxVisibleDistance.
Signal variants ($-suffixed) let the value change over time without
re-rendering the host component.
When <f-route> renders its matched component, it passes a route$ prop — a
signal holding { uid, url, state, params } (no handler, to avoid leaking
the router definition):
f(function myProfile ({ html, props }) {
const { npub } = props.route$().params
return html`<div>Profile: ${npub}</div>`
})It's also exposed as a closest store, so deeper descendants can read the same
signal via useClosestStore('<f-route>').
There is no hot reloading support but you may use server-sent events and a file watcher to reload the web app on development on file changes.
You should set the flag globalThis._F_SHOULD_RESTORE_STATE_ON_TAB_RELOAD to true
to remember hooks' state between tab reloads.
It temporarily stores the state on window.sessionStorage.
Depending on the way you manage your app routes, after clicking some links
different components may render on tab reload. To keep correct hook state,
add a fixed id to your components like <a-component id='f7dcce56a4bcf' /><b-component id='62e7ff6526ba' />
See /example/server.js or read
esbuild live reload docs
if you need a build step. Also see how we set that global flag to true
and listen to server-sent events at /example/index.html.
On reloads, useTask and useAsyncComputed have a isHotStart argument for the function
they use. You can do if (isHotStart) return to avoid re-running these hooks.
Other hooks have an additional shouldCache config you may set to false
to skip restore. For example: useSignal('test', { shouldCache: false })
There are exports available for toSignal, toComputed and toStore
for manual usage. They don't have the same options as their hook versions
such as shouldCache.
Hooks are easier to use because they are automatically discarded when the component unmounts.
toTask and toAsyncComputed are missing for now.