{ "version": "https://jsonfeed.org/version/1", "title": "xoid Blog", "home_page_url": "https://xoid.dev/blog", "description": "xoid Blog", "items": [ { "id": "https://xoid.dev/blog/introducing-prefill", "content_html": "
React ecosystem needs a primitive for something we do constantly: Preconfiguring components.
\nWe routinely take a base component and:
\nImagine styling a Material UI component based on your design system. Traditionally, you would do something like:
\nconst StyledAccordion = (props: React.ComponentProps<typeof MuiAccordion>) => {
return (
<MuiAccordion
{...props}
className={styles.container}
classes={{
trigger: styles.trigger,
article: styles.article,
}}
/>
)
}
Today, things like this are done with wrappers, higher order components, or styling systems. Yet, all are either ad-hoc, or improvized solutions.
\nprefill is a tiny utility that addresses this. (npm, GitHub)
Its purpose is to do one thing well:
\n\n\nCompose props before they reach a component.
\n
With it, the same example becomes:
\nimport { prefill } from 'prefill'
const StyledAccordion = prefill(MuiAccordion, {
className: styles.container,
classes: {
trigger: styles.trigger,
article: styles.article,
}
})
We get several things for free:
\nReact.ComponentProps, type-safe by default{...props} neededforwardRef needed, even in older React versionsAlso, we get 3 more things we have not previously thought:
\nclassName merged automaticallystyle objects merged automaticallyThis is not just convenience, this is true abstraction.
\nprefill Really IsAt its core, prefill is partial application applied to React components.
\nPartial application means:
\n\n\nFixing some arguments of a function and returning a new function that accepts the rest.
\n
Example:
\nconst add = (a, b) => a + b
const add5 = partial(add, 5) // (b) => 5 + b
prefill applies this idea to components.
Turn generic components into specialized ones by locking in props:
\nconst OutlinedButton = prefill(Button, { variant: 'outlined' })
If variant is a required prop → it becomes optional
You can use prefill just like a \"styled\" primitive, without having to adopt a specific CSS runtime.
const Button = prefill('button', {
className: styles.myButton,
})
And then bring your own styling:
\nWhen the second argument is a function, you can decide how the props will be used before they reach the component. This is very powerful.
\nimport cx from 'clsx'
const Button = prefill(
'button',
(props: { variant?: 'default' | 'danger' }) => ({
className: classes.variant[props.variant ?? 'default'],
})
)
Multi-variant components is one of the places where prefill really shines.
\nYou can even use hooks inside the config function:
\nconst Input = prefill('input', () => {
const { text, onChangeText } = useContext(MyContext)
return { value: text, onChange: onChangeText }
})
This turns a plain input element into a context-bound input with all the prop forwarding, ref passing, style and className merging benefits.
\nYou want a sane API:
\nconst Select = prefill(WeirdSelect, (props: {
value?: string
onChange?: (v: string) => void
}) => ({
selectedValue: props.value,
onValueChange: props.onChange,
}))
prefill was right in front of us the whole timeThe idea isn't entirely new. While designing it, I remembered that a close cousin already existed in styled-components, in the form of the .attrs API:
import styled from 'styled-components'
const Button = styled.button.attrs({
type: 'button',
disabled: true,
})`
background: red;
`
For many cases this works well. But it is tightly coupled to:
\nFor example, the following pattern is officially supported, but it can be a footgun:
\nimport styled from 'styled-components'
const Button = styled.button.attrs(props => ({
disabled: props.isDisabled
}))
Here's why: Here, isDisabled prop leaks straight through to the underlying DOM element. To avoid this, styled-components relies on conventions like prefixing “ambient” props with $, or on manually configuring a shouldForwardProp function.
With prefill, simply:
prefill('button', (props: { isDisabled?: boolean }) => ({
disabled: props.isDisabled
}))
isDisabled never leaks, because it was destructed from the props. When a prop is \"touched\", \"destructed\", \"used\"; it's automatically excluded from being passed down. No shouldForwardProp needed.
With prefill, prop hygiene becomes structural, rather than based on conventions.
styled as a special caseA minimal styled primitive can be built on top of prefill:
import { css } from 'emotion'
const styled = (as, style) => prefill(as, { className: css(style) })
This highlights an important point:
\nprefill is more fundamental than styled.\n\nNote: This is an analogy, not a replacement strategy. Some CSS-in-JS libraries require their own
\nstyledprimitive to integrate correctly with their runtime (e.g. theming, SSR, or style extraction).prefilldoes not aim to replace those. For zero-runtime solutions such as CSS modules or Tailwind though, you can safely use it as your mainstyledprimitive.
The problems we tried to solve with wrappers, \"connected\" components, HOCs, styled, .attrs often looked like they are unrelated.
prefill reframes that, and helps us see them from the lens of \"partial application\". Once you see that, many of these patterns stop feeling magical or ad-hoc, and prefill becomes an obvious go-to primitive in many cases.
If it sounds useful, you can try it today:
\nnpm install prefill
# or
pnpm add prefill
prefill on npm, prefill on GitHub
", "url": "https://xoid.dev/blog/introducing-prefill", "title": "`prefill`: Partial application for React components", "summary": "React ecosystem needs a primitive for something we do constantly: Preconfiguring components.", "date_modified": "2026-01-28T00:00:00.000Z", "author": { "name": "Onur Kerimov", "url": "https://github.com/onurkerimov" }, "tags": [] }, { "id": "https://xoid.dev/blog/introducing-xoid", "content_html": "xoid is a framework-agnostic state management library, about 1kB gzipped, designed around one idea: an API, with a low surface area, that can represent local state, global state, derived state, finite state machines, and even observable streams without switching mental models.
\nIn React especially, we routinely switch between different ways of thinking about state:
\nEach switch forces a context change. The APIs look different even when the underlying problem is the same.
\nMany companies still use Redux for global state, then there's reselect for derived state, maybe there's also RxJS used in sync with some parts of the Redux state. The problem is not necessarily any of these tools, but fragmentation across tools.
\nxoid is an attempt to collapse most of that surface area into a single primitive.
\nAn atom is a holder of state.
\nimport { atom } from 'xoid'
const $count = atom(3)
$count.set(5)
$count.update(s => s + 1)
Atoms expose their current value, allow immutable updates, and support subscriptions explicitly. There is no magic and no hidden dependency tracking.
\nAtoms can also define actions colocated with the state they operate on, which keeps mutation logic close to the data.
\nconst $count = atom(5, (a) => ({
increment: () => a.update(s => s + 1),
decrement: () => a.value--
}))
This already covers a large portion of what people use reducers for, without introducing a second abstraction.
\nDerived state is built into the core API.
\nxoid’s derived atoms were heavily inspired by Recoil. A derived atom declares how its value is computed from other atoms.
\nconst $a = atom(3)
const $b = atom(5)
const $sum = atom(read => read($a) + read($b))
Derived atoms are lazily evaluated. Their computation only runs when something actually subscribes. This keeps the runtime cost predictable.
\nFor the common case of deriving from a single atom, there is also a shortcut.
\nconst $double = $a.map(s => s * 2)
This .map method is not just syntactic sugar. It is one of the defining characteristics of xoid.
xoid includes two methods that are intentionally first-class: .focus and .map.
.focus acts like a lens into nested state.
const $state = atom({ deeply: { nested: { alpha: 5 } } })
const $alpha = $state.focus(s => s.deeply.nested.alpha)
$alpha.set(6)
Because updates are immutable, changing a focused branch replaces the root atom with a new value while preserving structural sharing.
\nThis makes nested state ergonomic without requiring reducers, immer, or structural cloning utilities.
\n.map, on the other hand, treats atoms as streams of values. Mapping an atom creates a derived atom that updates whenever the source updates.
Together, focus and map make it possible to work comfortably with deeply nested state and reactive transformations without pulling in an additional observable or stream library.
\nThis is a key difference from many atomic state libraries. There are excellent alternatives in this space, such as nanostores, but xoid deliberately leans into focus and map as core operations rather than extensions. The result is an API that handles nested state and reactive derivation in a single place.
\nAtoms expose two subscription methods: subscribe and watch.
const unsub = $atom.subscribe((state, prev) => {
console.log(state, prev)
})
Nothing is implicit. There is no hidden dependency graph. Subscriptions are straightforward callbacks with clear lifetimes.
\nThis makes atoms usable outside of UI frameworks just as easily as inside them.
\nxoid integrates with React, Vue, and Svelte using a single hook: useAtom.
There are no providers, no configuration steps, and no framework-specific concepts leaking into the core.
\nimport { useAtom } from '@xoid/react'
const value = useAtom(myAtom)
The same atom can be consumed from React, Vue, Svelte, or plain JavaScript without modification.
\nThis symmetry is intentional. The core library has no notion of components or renders.
\nIf that sounds appealing, the documentation and examples are all here.
\nCurious feedback is very welcome.
", "url": "https://xoid.dev/blog/introducing-xoid", "title": "Introducing xoid", "summary": "xoid is a framework-agnostic state management library, about 1kB gzipped, designed around one idea: an API, with a low surface area, that can represent local state, global state, derived state, finite state machines, and even observable streams without switching mental models.", "date_modified": "2026-01-28T00:00:00.000Z", "author": { "name": "Onur Kerimov", "url": "https://github.com/onurkerimov" }, "tags": [ "xoid", "announcement" ] } ] }