Doja is the first headless framework ever that lets you write components ONCE, then turn them into real Vue, React, or web components! This is achieved WITHOUT transpilation, plugins, or preprocessors.
By itself, Doja doesn't have a lifecycle, context, or JSX implementation. Doja's effect
or inject
exports connect to React's useEffect
or Vue's onMounted
/onUnmounted
under the hood. Similarly, the doja/jsx-runtime
is simply an empty object forwarded to React or Vue's own JSX runtimes. There are 3 packages at the moment:
doja-react
- React integrationdoja-vue
- Vue integration@doja/pce
- Web components using thepreact-custom-element
package
A Doja component is so simplistic, and you can use one of the following flavors:
import { create } from 'doja'
const Counter = () => {
let count = create(0)
return () => (
<>
{count.value}
<button onClick={() => count.value++}>+</button>
<button onClick={() => count.value--}>-</button>
</>
)
}
class Counter extends Doja {
count = 0
render = () => (
<>
{this.count}
<button onClick={() => this.count++}>+</button>
<button onClick={() => this.count--}>-</button>
</>
)
}
import $ from 'doja/macro'
const Counter = () => {
let count = $(0)
return () => (
<>
{count}
<button onClick={() => count++}>+</button>
<button onClick={() => count--}>-</button>
</>
)
}
doja/macro
is inspired by Svelte 5's Runes. It's a reactivity transform using thebabel-plugin-macros
.
Observe the following app:
~/components/App.tsx
// @jsxImportSource doja
import Doja, { create, effect, inject, type InjectionKey } from 'doja'
const theme = { background: 'pink' }
const ThemeSymbol: InjectionKey<typeof theme> = Symbol()
const Counter = (props: { initalValue: number }) => {
const theme = inject(ThemeSymbol)
const count = create(props.initialValue)
return () => (
<div style={{ background: theme.background }}>
{count.value}
<button onClick={() => count.value++}>+</button>
<button onClick={() => count.value--}>-</button>
</div>
)
}
Counter.observedAttributes = ['initialValue'] // Vue and Web components need this beforehand*
Counter.tagName = 'my-counter' // Web components need this
const ThemeProvider = Doja(ThemeSymbol)
const App = () => {
effect(() => {
console.log('i was mounted')
return () => console.log('i was unmounted')
})
return (
<ThemeProvider value={theme}>
<Counter initialValue={0} />
</ThemeProvider>
)
}
export { theme, ThemeSymbol, ThemeProvider, Counter, App }
*Hint: you can use ts-keysof.macro to automatically generate observed attributes from prop types
We can export them as React and Vue components simply as:
~/components-react/App.tsx
import toReact from 'doja-react'
import { App, Counter as CounterDoja, ThemeProvider as ThemeProviderDoja } from '~/components/App'
export const Counter = toReact(CounterDoja)
export const ThemeProvider = toReact(ThemeProviderDoja)
export default toReact(App)
~/components-vue/App.tsx
import toVue from 'doja-vue'
import { App, Counter as CounterDoja, ThemeProvider as ThemeProviderDoja } from '~/components/App'
export const Counter = toVue(CounterDoja)
export const ThemeProvider = toVue(ThemeProviderDoja)
export default toVue(App)
Alternatively, you can re-export the ThemeSymbol
for Vue users, since provide
/inject
is a common practice in Vue.
import { InjectionKey } from 'vue'
import { theme, ThemeSymbol as ThemeSymbolDoja } from '~/components/App'
export const ThemeSymbol = ThemeSymbolDoja as InjectionKey<typeof theme>
Here are the framework-specific APIs that you can replace with Doja:
Doja | React |
---|---|
import { create } from 'doja';
function Counter() {
const $count = create(0);
const increment = () => $count.value++;
const decrement = () => $count.value--;
return () => (
<div>
<p>Count: {$count.value}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
} |
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
} |
Doja | React |
---|---|
import { create, effect } from 'doja';
function MyComponent() {
const myInputRef = create();
effect(() => {
myInputRef.value.focus();
}, []);
return <input ref={myInputRef} />;
} |
import { useRef, useEffect } from 'react';
function MyComponent() {
const myInputRef = useRef();
useEffect(() => {
myInputRef.current.focus();
}, []);
return <input ref={myInputRef.set} />;
} |
No need for an extra hook such as
useRef
. Simply use the same atom creator function.
Doja | React |
---|---|
import { inject } from 'doja';
const MySymbol = Symbol();
function ParentComponent() {
return () => (
<MySymbol value={'Hello from Context!'}>
<ChildComponent />
</MySymbol>
);
}
function ChildComponent() {
const contextValue = inject(MySymbol);
return () => <div>{contextValue}</div>;
} |
import { createContext, useContext } from 'react';
const MyContext = createContext();
function ParentComponent() {
return (
<MyContext.Provider value={'Hello from Context!'}>
<ChildComponent />
</MyContext.Provider>
);
}
function ChildComponent() {
const contextValue = useContext(MyContext);
return <div>{contextValue}</div>;
} |
Doja can use ES6 symbols as a context provider, just like how Vue does.
Doja | React |
---|---|
const MyInput = (props, ref) => {
return () => <input ref={ref} />;
};
function ParentComponent() {
const myInputRef = useRef();
return () => <MyInput ref={myInputRef.set} />;
} |
import { forwardRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input ref={ref} />;
});
function ParentComponent() {
const myInputRef = useRef();
return <MyInput ref={myInputRef} />;
} |
Doja doesn't need a
forwardRef
export. If you define a component with a second argument, it'll simply use it to forward the ref.
Doja | React |
---|---|
import { create } from 'doja';
const ExampleComponent = () => {
const $count = create(0);
const $squared = $count.map((count) => {
console.log('Computing squared value...');
return count * count;
});
return () => (
<div>
<p>Count: {$count.value}</p>
<p>Squared Value: {$squared.value}</p>
<button onClick={() => $count.value++}>Increment</button>
</div>
);
}; |
import { useState, useMemo } from 'react';
const ExampleComponent = () => {
const [count, setCount] = useState(0);
const squaredValue = useMemo(() => {
console.log('Computing squared value...');
return count * count;
}, [count]);
return (
<div>
<p>Count: {count}</p>
<p>Squared Value: {squaredValue}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}; |
Doja | React |
---|---|
import { create } from 'react';
// Child component that forwards the ref
const ChildComponent = (props, ref) => {
const inputRef = create();
ref.value = {
focus: () => {
inputRef.value.focus();
},
}
return () => <input ref={inputRef.set} />;
};
// Parent component using ChildComponent with ref
function ParentComponent() {
const childRef = create();
const handleClick = () => {
// Call the exposed focus method on the child component
childRef.value.focus();
};
return () => (
<div>
<ChildComponent ref={childRef.set} />
<button onClick={handleClick}>Focus Input</button>
</div>
);
} |
import { useRef, useImperativeHandle, forwardRef } from 'react';
// Child component that forwards the ref
const ChildComponent = forwardRef((props, ref) => {
const inputRef = useRef();
// Expose only the focus method to the parent component
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
}));
return <input ref={inputRef} />;
});
// Parent component using ChildComponent with ref
function ParentComponent() {
const childRef = useRef();
const handleClick = () => {
// Call the exposed focus method on the child component
childRef.current.focus();
};
return (
<div>
<ChildComponent ref={childRef} />
<button onClick={handleClick}>Focus Input</button>
</div>
);
} |
Like we said: "Most hooks are complete bloat from a static closure's perspective.