Reagi is a react-like library for building UIs using a reactive programming model. It is a small library that is designed to be easy to use and understand. Supports hooks like useState and useEffect.
Based on the fiber architecture of React, it supports concurrent rendering.
To begin using Reagi, you can start with a single html file and include babel standalone to transpile JSX.
<!-- From https://react.dev/learn#updating-the-screen -->
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
</body>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel" data-type="module">
/** @jsx Reagi.createElement */
/** @jsxFrag Reagi.Fragment */
import Reagi, { createRoot, useState } from 'https://cdn.jsdelivr.net/gh/JasirZaeem/reagi@main/src/index.js';
function App() {
return (
<div>
<h1>Counter</h1>
<CounterButton />
</div>
);
}
function CounterButton() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
Clicked {count} times
</button>
);
}
const root = createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</html>Reagi supports four hooks:
SOURCEJasirZaeem/reagi/src/hooks/useState.js
useState lets you add state to your components. It returns an array with the current state value and a function that lets you update it. State is preserved between re-renders, and updating it will trigger a re-render. Multiple state updates are batched and applied in a single render asynchronusly.
The evergreen counter example
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
Clicked {count} times
</button>
);SOURCEJasirZaeem/reagi/src/hooks/useEffect.js
useEffect lets you synchronize components with an external system. E.g. controlling non reagi components or libraries based on react state, make network requests, or set up subscriptions, etc. Effects are run after rendering. A cleanup function can be returned to clean up the effect when the component unmounts or before running the effect again.
Synchronize the document title with the count state.
useEffect(() => {
document.title = `Clicked ${count} times`;
return () => {
// Reset the title on unmount
document.title = "Reagi App";
};
}, [count]);SOURCEJasirZaeem/reagi/src/hooks/useMemo.js
useMemo lets you cache results of computations to persist across renders. It takes a function and an array of dependencies. The function is only re-run when the dependencies change.
Persist search results across renders if the search query remains the same. In the following code the data will not be filtered again on component re-render is the searchQuery (or tha data) does change.
const searchResults = useMemo(() => {
return data.filter(item => item.includes(searchQuery));
}, [data, searchQuery]);SOURCEJasirZaeem/reagi/src/hooks/useRef.js
useRef lets you persist and reference a value across renders that is not needed for rendering. It returns a mutable object with a current property that can be assigned to. The returned object is referentially stable across renders.
Referencing a setTimeout id across renders.
const timeoutId = useRef(null);
const handleStartCountdown = () => {
if (timeoutId.current) {
clearTimeout(timeoutId.current);
}
timeoutId.current = setTimeout(() => {
// ...
}, 10000);
}
const handleStopCountdown = () => {
clearTimeout(timeoutId.current);
}<!-- From https://react.dev/learn/tutorial-tic-tac-toe -->
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
</body>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel" data-type="module">
/** @jsx Reagi.createElement */
/** @jsxFrag Reagi.Fragment */
import Reagi, { createRoot, useState } from 'https://cdn.jsdelivr.net/gh/JasirZaeem/reagi@main/src/index.js';
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = 'X';
} else {
nextSquares[i] = 'O';
}
onPlay(nextSquares);
}
const winner = calculateWinner(squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (xIsNext ? 'X' : 'O');
}
return (
<>
<div className="status">{status}</div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}
let App = function Game() {
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const xIsNext = currentMove % 2 === 0;
const currentSquares = history[currentMove];
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
}
function jumpTo(nextMove) {
setCurrentMove(nextMove);
}
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = 'Go to move #' + move;
} else {
description = 'Go to game start';
}
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}
function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
const root = createRoot(document.getElementById('root'));
root.render(<App />);
</script>
<style>
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
margin: 20px;
padding: 0;
}
h1 {
margin-top: 0;
font-size: 22px;
}
h2 {
margin-top: 0;
font-size: 20px;
}
h3 {
margin-top: 0;
font-size: 18px;
}
h4 {
margin-top: 0;
font-size: 16px;
}
h5 {
margin-top: 0;
font-size: 14px;
}
h6 {
margin-top: 0;
font-size: 12px;
}
code {
font-size: 1.2em;
}
ul {
padding-inline-start: 20px;
}
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
margin: 20px;
padding: 0;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.board-row:after {
clear: both;
content: '';
display: table;
}
.status {
margin-bottom: 10px;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
</style>
</html>220 smtp.example.com ESMTP Postfix HELO client.example.com 250 smtp.example.com MAIL FROM: <me@mydomain.com> 250 Ok RCPT TO: <hi@zaeem.dev> 250 Ok DATA 354 End data with <CR><LF>.<CR><LF> From: "Me" <me@mydomain.com> To: "Jasir" <hi@zaeem.dev> Date: $Tue, 16 Sep 2025 17:17:43 GMT Subject: Complaint Mail Hi Jasir,Your site gave me a virus. I am very upset.
Yours truly,
Me
.
250 Ok: queued as 12345
QUIT
221 Bye
You can reach me at mailto:hi@zaeem.dev