The lightweight and powerful UI rendering engine without dependencies and written in TypeScript (Browser, Node.js, Android, iOS, Windows, Linux, macOS)
npm install @dark-engine/core
npm install @dark-engine/core
`
yarn:
`
yarn add @dark-engine/core
`
CDN:
`html
`
Table of contents
- API
- Elements
- JSX
- Components
- Conditional rendering
- List rendering
- Recursive rendering
- Hooks
- Effects
- Memoization
- Refs
- Catching errors
- Context
- Batching
- Code splitting
- Async rendering
- Concurrent rendering
- Hot module replacement
- Others
API
`tsx
import {
type CommentVirtualNode,
type Component,
type ComponentFactory,
type DarkElement,
type Dispatch,
type ElementKey,
type FunctionRef,
type MutableRef,
type Reducer,
type Ref,
type StandardComponentProps,
type TagVirtualNode,
type TextVirtualNode,
type VirtualNodeFactory,
batch,
Comment,
component,
createContext,
detectIsServer,
ErrorBoundary,
Fragment,
Guard,
h,
hot,
lazy,
memo,
startTransition,
Suspense,
Text,
useCallback,
useContext,
useDeferredValue,
useEffect,
useError,
useEvent,
useId,
useImperativeHandle,
useInsertionEffect,
useLayoutEffect,
useMemo,
useReducer,
useRef,
useState,
useSyncExternalStore,
useTransition,
useUpdate,
View,
VERSION,
} from '@dark-engine/core';
`
Π‘ore concepts...
Elements
Elements are a collection of platform-specific primitives and components. For the browser platform, these are tags, text, and comments.
#### View, Text, Comment
`tsx
import { View, Text, Comment } from '@dark-engine/core';
import { createRoot } from '@dark-engine/platform-browser';
`
`tsx
const h1 = (props = {}) => View({ ...props, as: 'h1' });
const content = [h1({ slot: Text(I'm the text inside the tag) }), Comment(I'm the comment)];
createRoot(document.getElementById('root')).render(content);
`
JSX
JSX is a syntax extension for JavaScript that lets you write HTML-like markup inside a JavaScript file.
$3
In your tsconfig.json, you must add these rows:
`json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@dark-engine/core",
}
}
`
The necessary functions will be automatically imported into your code.
`tsx
const content = (
<>
I'm the text inside the tag
Hello
>
);
createRoot(document.getElementById('root')).render(content);
`
Components
Components are the fundamental logical units of a modern interface. Components can accept props, have their own internal state, and contain child elements or components.
`tsx
type SkyProps = {
color: string;
};
const Sky = component(({ color }) => {
return color: ${color}}>My color is {color};
});
;
`
A component can return an array of elements:
`tsx
const App = component(props => {
return [
Header ,
Content,
,
];
});
`
You can also use Fragment as an alias for an array:
`tsx
return (
Header
Content
);
`
or
`tsx
return (
<>
Header
Content
>
);
`
If a child element is passed to the component, it will appear in props as slot:
`tsx
const App = component(({ slot }) => {
return (
<>
Header
{slot}
>
);
});
Content ;
`
Conditional rendering
`tsx
const App = component(({ isOpen }) => {
return isOpen ? Hello : null
});
`
`tsx
const App = component(({ isOpen }) => {
return (
<>
Hello
{isOpen && Content}
>
);
});
`
`tsx
const App = component(({ isOpen }) => {
return (
<>
Hello
{isOpen ? : }
world
>
);
});
`
List rendering
`tsx
const List = component(({ items }) => {
return (
<>
{items.map(x => {item.name})}
>
);
});
`
`tsx
const List = component(({ items }) => {
return items.map(x => {x.title});
});
`
Recursive rendering
You can put components into themself to get recursion if you want. But every recursion must have return condition for out. In other case we will have infinity loop. Recursive rendering might be useful for tree building or something else.
`tsx
const Item = component(({ level, current = 0 }) => {
if (current === level) return null;
return (
margin-left: ${current === 0 ? '0' : '10px'}}>
level: {current + 1}
);
});
const App = component(() => {
return ;
});
`
`
level: 1
level: 2
level: 3
level: 4
level: 5
`
Hooks
Hooks are needed to bring components to life: give them an internal state, start some actions, and so on. The basic rule for using hooks is to use them at the top level of the component, i.e. do not nest them inside other functions, cycles, conditions. This is a necessary condition, because hooks are not magic, but work based on array indices.
There are three types of main hooks:
- A hook that allows you to store the state of a component between renders.
- A hook that starts the process of rerendering a component.
- A hook that triggers side effects.
All other hooks are somehow derived from these hooks.
#### useState
The hook to store the state and call to update a piece of the interface.
`tsx
const App = component(() => {
const [count, setCount] = useState(0);
return ;
});
`
The setter can take a function as an argument to which the previous state is passed:
`tsx
const handleClick = () => setCount(x => x + 1);
`
#### useReducer
It's used when a component has multiple values in the same complex state, or when the state needs to be updated based on its previous value.
`tsx
type State = { count: number };
type Action = { type: string };
const initialState: State = { count: 0 };
function reducer(state: State, action: Action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
const App = component(() => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
>
);
});
`
#### useUpdate
Simply starts the component rerender.
`tsx
const update = useUpdate();
console.log('render');
return (
<>
>
);
`
Effects
Side effects are useful actions that take place outside of the interface rendering. For example, side effects can be fetch data from the server, calling timers, subscribing.
#### useEffect
Executed asynchronously, after rendering.
`tsx
const [albums, setAlbums] = useState>([]);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/albums')
.then(x => x.json())
.then(x => setAlbums(x));
}, []);
if (albums.length === 0) return loading...;
return (
{albums.map(x => - {x.title}
)}
);
`
The second argument to this hook is an array of dependencies that tells it when to restart. This parameter is optional, then the effect will be restarted on every render.
Also this hook can return a reset function:
`tsx
useEffect(() => {
const timerId = setTimeout(() => {
console.log('hey!');
}, 1000);
return () => clearTimeout(timerId);
}, []);
`
#### useLayoutEffect
This type of effect is similar to useEffect, however, it is executed synchronously right after the commit phase of new changes. Use this to read layout from the DOM and synchronously re-render.
`tsx
useLayoutEffect(() => {
const height = rootRef.current.clientHeight;
setHeight(height);
}, []);
`
#### useInsertionEffect
The signature is identical to useEffect, but it fires synchronously before all DOM mutations. Use this to inject styles into the DOM before reading layout in useLayoutEffect. This hook does not have access to refs and cannot call render. Useful for css-in-js libraries.
`tsx
useInsertionEffect(() => {
// add style tags to head
}, []);
`
Memoization
Memoization in Dark is the process of remembering the last value of a function and returning it if the parameters have not changed. Allows you to skip heavy calculations if possible.
#### useMemo
The hook for memoization of heavy calculations or heavy pieces of the interface:
`tsx
const memoizedOne = useMemo(() => Math.random(), []);
const memoizedTwo = useMemo(() => {Math.random()}, []);
<>
{memoizedOne}
{memoizedTwo}
>
`
#### useCallback
Suitable for memoizing handler functions descending down the component tree:
`tsx
const handleClick = useCallback(() => setCount(count + 1), [count]);
`
#### useEvent
Similar to useCallback but has no dependencies. Ensures the return of the same function, with the closures always corresponding to the last render. In most cases, it eliminates the need to track dependencies in useCallback.
`tsx
const handleClick = useEvent(() => setCount(count + 1));
`
$3
`tsx
const Memo = memo(component(() => {
console.log('Memo render!');
return I'm Memo;
}));
const App = component(() => {
console.log('App render!');
useEffect(() => {
setInterval(() => root.render( ), 1000);
}, []);
return (
<>
>
);
});
const root = createRoot(document.getElementById('root'));
root.render( );
`
`
App render!
Memo render!
App render!
App render!
App render!
...
`
As the second argument, it takes a function that answers the question of when to re-render the component:
`tsx
const Memo = memo(Component, (prevProps, nextProps) => prevProps.color !== nextProps.color);
`
####
A component that is intended to mark a certain area of the layout as static, which can be skipped during updates. Based on memo.
`tsx
I'am always static
`
Refs
To get full control over components or DOM nodes Dark suggests using refs.
#### useRef
`tsx
const rootRef = useRef(null);
useEffect(() => {
rootRef.current.focus();
}, []);
;
`
Also there is support function refs
`tsx
console.log(ref)} />;
`
#### useImperativeHandle
They are needed to create an object inside the reference to the component in order to access the component from outside:
`tsx
type ChildProps = {
ref?: Ref;
}
type ChildRef = {
hello: () => void;
};
const Child = component(({ ref }) => {
useImperativeHandle(
ref,
() => ({
hello: () => console.log('hello!'),
}),
[],
);
return I'm Child;
});
const App = component(() => {
const childRef = useRef(null);
useEffect(() => {
childRef.current.hello();
}, []);
return ;
});
`
Catching errors
When you get an error, you can log it and show an alternate user interface.
####
Catching errors is only enabled on the client, and is ignored when rendering on the server. This is done intentionally, because on the server, when rendering to a stream, we cannot take back the HTML that has already been sent.
`tsx
const Broken = component(() => {
throw new Error();
});
const App = component(() => {
return (
<>
I'm OK π
Ooops! π³
also has a renderFallback and onError prop.
#### useError
`tsx
const Counter = component<{ count: number }>(({ count }) => {
if (count === 3) {
throw new Error('oh no!');
}
return {count};
});
const App = component(() => {
const [count, setCount] = useState(0);
const [error, reset] = useError();
if (error) {
return (
<>
Something went wrong! π«’
>
)
};
return (
<>
>
);
});
`
Context
The context might be useful when you need to synchronize state between deeply nested elements without having to pass props from parent to child.
#### createContext and useContext
`tsx
type Theme = 'light' | 'dark';
const ThemeContext = createContext('light');
const useTheme = () => useContext(ThemeContext);
const CurrentTheme = component(() => {
const theme = useTheme();
return {theme === 'light' ? 'βοΈ' : 'π'};
});
const App = component(() => {
const [theme, setTheme] = useState('light');
const handleToggle = () => setTheme(x => (x === 'dark' ? 'light' : 'dark'));
return (
);
});
`
If the context consumer is inside a memoized component that will skip the render from above when the context changes, then the consumer will automatically apply its internal render to apply the latest changes.
Batching
The strategy that allows you to merge similar updates into one render to reduce computational work.
#### batch
`tsx
useEffect(() => {
const handleEvent = (e: MouseEvent) => {
batch(() => {
setClientX(e.clientX);
setClientY(e.clientY); // render just this time
});
};
document.addEventListener('mousemove', handleEvent);
return () => document.removeEventListener('mousemove', handleEvent);
}, []);
`
Code splitting
#### lazy and
If your application is structured into separate modules, you may wish to employ lazy loading for efficiency. This can be achieved through code splitting. To implement lazy loading for components, dynamic imports of the component must be wrapped in a specific function - lazy. Additionally, a Suspense component is required to display a loading indicator or skeleton screen until the module has been fully loaded.
`tsx
const Page = lazy(() => import('./page'));
const App = component(() => {
const [isNewPage, setIsNewPage] = useState(false);
return (
<>
{isNewPage && (
Loading...
Async rendering
Dark is designed with support for asynchronous rendering. This implies that following the mounting of each component during the reconciliation phase, the core checks if a preset deadline has been reached. If the deadline is met, control of the event loop is yielded to other code, resuming at the next tick. If the deadline is not yet met, the core continues the rendering process. The deadline is consistently set at 6 milliseconds.
By default, core runs in synchronous mode, however, if Dark understands that it is rendering on the server, it goes into asynchronous mode and can wait for lazy modules to load and asynchronous code to execute if the useQuery hook from @dark-engine/data package is used.
Concurrent rendering
Concurrent rendering is a strategy that enables the assignment of the lowest priority to updates, allowing them to execute in the background without obstructing the main thread. Upon the arrival of higher-priority updates, such as user render events, the low-priority task is halted and retains all essential data for potential reinstatement (or permanent cancellation) by the scheduler, if there won't be further high-priority updates. This technique facilitates the creation of fluid user interfaces.
#### startTransition
Marks the update as low-priority and renders it in the background. This allows you to switch between tabs quickly, even if they are rendered slowly due to the large number of calculations. When switching tabs, unnecessary work is marked as obsolete and removed from the task list.
`tsx
const selectTab = (name: string) => startTransition(() => setTab(name));
<>
selectTab('about')}>
About
selectTab('posts')}>
Posts
selectTab('contact')}>
Contact
{tab === 'about' && }
{tab === 'posts' && }
{tab === 'contact' && }
>
`
#### useTransition
Allows you to create a version of startTransition and a flag isPending that will show what stage of concurrent rendering we are in.
`tsx
const [isPending, startTransition] = useTransition();
const handleClick = () => startTransition(() => onClick());
;
`
#### useDeferredValue
This fixes an issue with an unresponsive interface when user input occurs, based on which heavy calculations or heavy rendering is recalculated.
Returns a delayed value that may lag behind the main value. It can be combined with each other and with useMemo and memo for amazing responsiveness results...
`tsx
const [name, setName] = useState('');
const deferredName = useDeferredValue(name);
const isStale = name !== deferredName;
const handleInput = e => setName(e.target.value);
//
should be a memo component
return (
);
`
Hot Module Replacement (HMR)
Allows you to avoid reloading the entire interface when changing code in development mode. Saves the state of other components, because under the hood, instead of reloading the page, it simply performs a new render as if the interface was rebuilt not by new code, but by the user.
#### hot
`tsx
// index.tsx
import { hot } from '@dark-engine/core';
import { createRoot } from '@dark-engine/platform-browser';
import { App } from './app';
if (import.meta.webpackHot) {
import.meta.webpackHot.accept('./app', () => {
hot(() => root.render( ));
});
}
const root = createRoot(document.getElementById('root'));
root.render( );
`
Others
#### useId
The hook for generating unique identifiers that stable between renders.
`tsx
const id = useId();
// generates something like this 'dark:0:lflt'
<>
>
`
#### useSyncExternalStore
It's useful for synchronizing render states with an external state management library such as Redux.
`tsx
const state = useSyncExternalStore(store.subscribe, store.getState); // redux store
``