thenameisf
v1.0.6
Published
Javascript framework with both signals and React API that uses Web Components instead of JSX
Maintainers
Readme
F
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.
Motivation
I didn't want to learn new javascript front-end frameworks.
Alternatives
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.
Installation
npm i thenameisf
Usage
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>Functional
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.
Class-based
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>`
}
})Available React-Compatible Hooks
- useCallback
- useEffect
- useInsertionEffect
- useMemo
- useRef
- useState
Extra Features
useGlobalState for shared state
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)useClosestState hook instead of useContext
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')Simplified event callbacks
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} />`No need to mutate array if its length changes
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
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
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).
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 }
})useComputed
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())useStore
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$: function () { return this.count$.get() * 2 },
notReactive: 'Things won\'t re-render if I change',
deep: {
// signal
example$: 3
},
aSignal$: { anotherSignal$: 'signal\'s signal' }
})Lazy Initial Values
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
})()
})useGlobalStore
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')useGlobalSignal
Same thing as useGlobalStore but for storing a single signal.
const example$ = useGlobalSignal('<namespace>', <initial value>)useGlobalComputed
Same thing as useGlobalStore but for storing a single computation.
const example$ = useGlobalComputed('<namespace>', <function>)useClosestStore
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')useClosestSignal
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>')useClosestComputed
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>')useTask
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))
})useAsyncComputed
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>
`
})useUntrackedCallback
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()useStateSignal
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)Component
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 ({ html }) {
const users$ = useSignal(['Alice', 'Bob'])
const fieldsToBeMergedToBelowProps = { user, test: 'Hi!' }
return html`${users$.get().map(user => html`
<f-to-signals key=${user} props=${{
from: ['user'], // the user field will be turned into a user$ signal prop
...fieldsToBeMergedToBelowProps,
render: props => { return html`<div>Hello ${props.user$.get()} - ${props.test}</div>` }
}} />`
`)}`
})useSignalState
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$) // 6Hot Reloading
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 })
Ad-hoc Usage
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.
