React ecosystem needs a primitive for something we do constantly: Preconfiguring components.
We routinely take a base component and:
- lock in some props
- inject styles or behavior
- and export a more specialized version
Imagine styling a Material UI component based on your design system. Traditionally, you would do something like:
const 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.
prefill is a tiny utility that addresses this. (npm, GitHub)
Its purpose is to do one thing well:
Compose props before they reach a component.
With it, the same example becomes:
import { prefill } from 'prefill'
const StyledAccordion = prefill(MuiAccordion, {
className: styles.container,
classes: {
trigger: styles.trigger,
article: styles.article,
}
})
We get several things for free:
- No need for
React.ComponentProps, type-safe by default - No
{...props}needed - No
forwardRefneeded, even in older React versions
Also, we get 3 more things we have not previously thought:
classNamemerged automaticallystyleobjects merged automatically- Component displayName is preserved in React Devtools
This is not just convenience, this is true abstraction.
What prefill Really Is
At its core, prefill is partial application applied to React components.
Partial application means:
Fixing some arguments of a function and returning a new function that accepts the rest.
Example:
const add = (a, b) => a + b
const add5 = partial(add, 5) // (b) => 5 + b
prefill applies this idea to components.
Core Use Cases
1. Prefilled Components
Turn generic components into specialized ones by locking in props:
const OutlinedButton = prefill(Button, { variant: 'outlined' })
If variant is a required prop → it becomes optional
2. Styling
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:
- CSS Modules
- Emotion
- Tailwind
- Vanilla CSS
3. Variants
When the second argument is a function, you can decide how the props will be used before they reach the component. This is very powerful.
import 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.
4. Context binding
You can even use hooks inside the config function:
const 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.
5. Prop renaming / API Adapters
You want a sane API:
const 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 time
The 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:
- Their CSS-in-JS model
- styled-components runtime, and the bundle size that comes with it.
- Their own prop forwarding logic
For example, the following pattern is officially supported, but it can be a footgun:
import 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 case
A 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:
prefillis more fundamental thanstyled.
Note: This is an analogy, not a replacement strategy. Some CSS-in-JS libraries require their own
styledprimitive 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.
Closing thoughts
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:
npm install prefill
# or
pnpm add prefill
