PreffX is a self-confident JS library for creating reactive DOM. It is inspired by React, Preact and SolidJS, but offers its own signal-based approach.
- React-like JSX syntax;
- Preact signals as reactivity core;
- only signals cause rerender;
- each component is executed only once;
- component props come as the first argument and all utilities come as the second argument - there is no need to import them,
- both sync and async function components support;
- distinction between properties and attributes (all properties are prefixed with
$) - for example,$valueis a property, andvalueis an attribute.
The recommended way is to use the create-preffx interactive utility, which allows you to define name, language, and CSS solution for the project:
npx create-preffxAlternatively you can use degit:
npx degit msabitov/vite-preffx preffx-starter
cd preffx-starter
npm install
npm run devYou can also try StackBlitz demo
Create a root and mount your component:
import { createRoot } from 'preffx';
import { App } from './App';
createRoot().mount(App, { node: document.getElementById('app')! });Define the component — props and utilities come as arguments:
import type { PC } from 'preffx';
export const App: PC = (props, { signal }) => {
const count = signal(0);
return (
<button onClick={() => { count.value += 1 }}>
Count is {count}
</button>
);
};- Simple counter:
import type { PC } from 'preffx';
export const App: PC = (props, { signal }) => {
const count = signal(0);
return <button
onClick={() => {
count.value += 1
}}
>
Count is {count}
</button>
};- How to create element refs:
import type { PC } from 'preffx';
export const App: PC = (props, { signal }) => {
const signalRef = signal();
return <div $ref={signalRef}>
<div
$ref={(refVal) => {
// it will be called after element mounted with refVal = HTMLDivElement
// and before element destroyed with refVal = null
}}
$onMount={(refVal) => {
// it will be called after element mounted with refVal = HTMLDivElement
}}
$onDestroy={() => {
// it will be called before element destroyed with refVal = HTMLDivElement
}}
>
Refs
</button>
</div>
};- Lifecycle hooks:
import type { PC } from 'preffx';
export const App: PC = (props, { onMount, onDestroy }) => {
const { count } = props;
onMount(() => {
// some mount logic
});
onDestroy(() => {
// some destroy logic
});
return <button
onClick={() => {
count.value += 1
}}
>
Count is {count}
</button>
};- List rendering:
import type { PC } from 'preffx';
export const App: PC = (props, { For }) => {
const items = signal([
{
name: 'First'
},
{
name: 'Second'
}
]);
return <ul>
<For
items={items}
callback={(item) => <li>Item with name: {item.name}</li>}
fallback={<li>No items</li>}
/>
</ul>;
};- Context handling:
import type { PC } from 'preffx';
const contextKey = 'ctx-counter';
const AnotherComponent: PC = (props, { context }) => {
// read context
const counter = context[contextKey];
return <span>
{counter}
</span>;
};
export const App: PC = (props, { signal, context }) => {
const counter = signal(0);
// modify context
context[contextKey] = counter;
return <p>
{valueFromContext}
<AnotherComponent />
</p>;
};- Async components:
import type { APC } from 'preffx';
import { getData } from './data';
import { AnotherComponent, AnotherAsyncComponent } from './components';
const AsyncComponent: APC<{
name: string;
}> = async (props, utils) => {
const data = await getData()
const componentRoot = await <AnotherAsyncComponent name='nested'/>;
return <div>
<AnotherComponent data={data} />
{componentRoot}
</div>;
}- Error handling:
import type { PC } from 'preffx';
import { AnotherComponent } from './components';
export const App: PC = (props, { Catch }) => {
return <div>
Sometimes components return errors
<Catch fallback={<div>Catched!</div>}>
<AnotherComponent />
</Catch>
</div>;
};- Defered value handling:
import type { PC } from 'preffx';
import { AsyncComponent } from './components';
export const App: PC = (props, { computed, Defer }) => {
const def = computed(() => <AsyncComponent id={props.id} />);
return <Defer
value={def}
initial={<div>Please wait</div>}
/>;
};- Portal:
import type { PC } from 'preffx';
export const App: PC = (props, { Portal }) => {
return <>
<span>Some text inside current tree</span>
<Portal root={document.getElementById('portal')}>
<div>Text inside portal</div>
</Portal>
</>;
};- Unique identifiers:
import type { PC } from 'preffx';
export const App: PC = (props, { id }) => {
// get unique id
const inputId = id();
return <>
<label>
Password:
<input
type="password"
aria-describedby={inputId}
/>
</label>
<p id={inputId}>
The password should contain at least 18 characters
</p>
</>;
};- Simple routing:
import type { PC } from 'preffx';
export const App: PC = (props, { computed, url }) => {
const routeContent = computed(() => {
// depends on url signal
switch(url.value.pathname) {
case '/home':
return <div>Home page content</div>
case '/contacts':
return <div>Contacts page content</div>;
default:
return <div>Other page content</div>
}
});
return <div>
<a href='/home'>Home</a>
<a href='/contacts'>Contacts</a>
{routeContent}
</div>;
};- Advanced routing:
import type { PC } from 'preffx';
export const App: PC = (props, { routes }) => {
const routesContent = routes({
// root
'/': () => <div>Home page content</div>,
// route params
'/:lang?/user/:id': (_, {routeParams}) => <div>User #{routeParams.id} ({routeParams.lang || 'en'})</div>,
// fallback
'*': () => <div>Not found</div>
});
return <div>
<a href='/'>Home</a>
<a href='/user/42'>User page</a>
<a href='/ru/user/42'>User page (ru)</a>
<a href='/inknown'>Unknown page</a>
{routesContent}
</div>;
};- Simple i18n example:
import type { PC } from 'preffx';
const dictionary = {
en: {
openProfile: 'Open profile',
showStats: 'Show statistics'
},
ru: {
openProfile: 'Открыть профиль',
showStats: 'Показать статистику'
}
};
export const App: PC = (props, { computed, lang }) => {
const captions = computed(() => {
// depends on lang signal
return dictionary[lang.value] || dictionary.en;
});
return <div>
<button>{captions.openProfile}</button>
<button>{captions.showStats}</button>
</div>;
};- Advanced i18n example:
import type { APC } from 'preffx';
const getRuDictionary = async () => {
// there can be dynamic imports
return {
openProfile(name: string) {
return 'Открыть профиль № ' + name;
},
showStats: 'Показать статистику'
};
};
const getEnDictionary = async () => {
// there can be dynamic imports
return {
openProfile(name: string) {
return 'Open profile № ' + name;
},
showStats: 'Show stats'
};
};
export const App: APC = async (props, { dict, setLang }) => {
const initialDict = await getEnDictionary();
// locales need to be passed with dictionary resolvers,
// resolvers can be async
const captions = dict({
ru: getRuDictionary,
en: getEnDictionary
}, initialDict);
return <div>
<button onClick={() => setLang('en')}>EN</button>
<button onClick={() => setLang('ru')}>RU</button>
<span>
<button>{captions.openProfile('42')}</button>
<button>{captions.showStats}</button>
</span>
</div>;
};