The React ecosystem has had plenty of contenders over the years, but every now and then, something shows up that actually feels different. RippleJS isn’t just another “React but better” framework; it’s a genuine rethink of how we write UI code, built by someone who’s been deep in the guts of React and Svelte.
Created by Dominic Gannaway (yes, the guy who worked on React Hooks at Meta, created Lexical, authored Inferno, and was a core maintainer of Svelte 5), RippleJS is what happens when someone with serious framework mileage decides to start over and apply all the lessons at once. The wild part? He built the prototype in under a week (vibe coding definitely did some heavy lifting there).
If you want to hear Dominic walk through his thinking in his own words, the PodRocket team interviewed him about RippleJS and the ideas behind it. You can listen to that episode here: RippleJS with Dominic Gannaway on PodRocket.
Goal
In this article, we’ll look at what makes RippleJS different, build a to-do list (the “Hello World” of reactivity) to see it in action, and talk about whether it’s worth your attention.
You’ll need:
- Basic JavaScript/TypeScript knowledge
- Some HTML/CSS familiarity
- React experience is helpful, mainly because we’ll compare the two later
The core philosophy: TypeScript-first, not TypeScript-added
First big clarification: RippleJS does not mean you stop writing TypeScript. It means TypeScript is baked into the framework from day one, not bolted on after the fact.
In React:
- You write
.tsxfiles (TypeScript + JSX) - TypeScript checks your types
- JSX is just syntax sugar; React doesn’t really “understand” your reactive patterns at the type level
- You end up with generic types like
useState<number>that don’t know much about your actual state logic
In RippleJS:
- You write
.ripplefiles, which are essentially a superset of JSX designed for TypeScript - The compiler understands both your TypeScript types and your reactive state patterns
- You get better autocompletion, error checking, and tooling support because the framework knows what
track()means - The
.rippleextension is meant to create a better DX “not only for humans, but also for LLMs”
That’s a pretty fundamental shift in how tooling, error messages, and developer experience are designed, especially in a world where TypeScript is mostly expected by default.
Here’s a concrete example:
// React (.tsx) - TypeScript is external to React's reactivity
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // Generic type, React doesn't "know" about count's reactive nature
const double = count * 2; // Not reactive, just a calculation
return (
<div>
<p>Count: {count}</p>
<p>Double: {double}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// RippleJS (.ripple) - TypeScript integrated with reactivity
import { track } from 'ripple';
export component Counter() {
let count = track(0); // Compiler knows this is reactive
let double = track(() => @count * 2); // Compiler knows this derives from count
<div>
<p>{"Count: "}{@count}</p>
<p>{"Double: "}{@double}</p>
<button onClick={() => @count++}>{"Increment"}</button>
</div>
}
In RippleJS, the compiler understands that double is derived from count. If you try to use @double somewhere that doesn’t make sense, or if you forget the @ when accessing a tracked value, the compiler catches it immediately with precise error messages.
In React, TypeScript mostly just sees regular variables and functions. It can’t reason about reactivity in the same way. That said, we shouldn’t pretend the Ripple syntax isn’t a bit more visually “busy” than React at first glance; it is.
Where RippleJS currently stands
Let’s be honest about what RippleJS is and isn’t right now.
What it has:
- Very good performance and memory usage
- Full TypeScript integration with type checking
- VSCode extension with diagnostics and IntelliSense
- Prettier formatting support
- Component-level scoped CSS
What it doesn’t have (yet):
- SSR (Server-Side Rendering) – currently SPA-only
- Full production testing across various use cases
- A massive ecosystem of third-party libraries
What makes it interesting, anyway, is the direction. There’s a plan to integrate SSR and to wire AI into the dev server to help diagnose “page-like” issues. In a world where everyone is using AI coding tools, a framework designed with AI assistance in mind from the start is intriguing.
Ripple’s component model
If you’ve been writing React for a while now, RippleJS will feel a lot similar but different in some ways. One of the biggest mental shifts: components don’t return JSX; they are the JSX.
Let me show you what I mean:
// React - Components return JSX
function Button({ text, onClick }) {
return (
<button onClick={onClick}>
{text}
</button>
);
}
function App() {
return (
<div>
<Button text="Click me" onClick={() => console.log("Clicked!")} />
</div>
);
}
// RippleJS - Components ARE JSX (imperative style)
component Button(props: { text: string, onClick: () => void }) {
<button onClick={props.onClick}>
{props.text}
</button>
}
export component App() {
<div>
<Button text="Click me" onClick={() => console.log("Clicked!")} />
</div>
}
Notice the component keyword instead of function? Notice how there’s no return statement? This is because templates in RippleJS are statements rather than expressions. You’re not computing a value to return; you’re declaring what the component is.
A small syntax change of this sort could yield better results on how the compiler optimizes your code.
Why does no component re-rendering matter?
Here’s where RippleJS starts to pull ahead of React in interesting ways. In React, when state changes, the entire component function re-runs. Sure, React is smart about what actually updates in the DOM, but the JavaScript still executes.
In RippleJS? Reactivity means only the specific parts of the DOM that depend on the changed state get updated. There is no component re-rendering or reconciliation. Just surgical updates to exactly what needs to change.
This is similar in spirit to Solid and Svelte 5, but RippleJS aims to get there with a cleaner, TypeScript-centric API.
How reactivity actually works: track() and the @ Symbol
RippleJS’s reactivity system revolves around two ideas:
track()for creating reactive values@for reading/writing those values
import { track } from 'ripple';
export component Counter() {
// Create a reactive value
let count = track(0);
// Create a derived reactive value
let double = track(() => @count * 2);
let quadruple = track(() => @double * 2);
<div>
<p>{"Count: "}{@count}</p>
<p>{"Double: "}{@double}</p>
<p>{"Quadruple: "}{@quadruple}</p>
<button onClick={() => @count++}>{"Increment"}</button>
</div>
}
- track(0) creates a reactive primitive with initial value 0
track(() => @count * 2)creates a computed value that automatically updates whencountchanges@countis how you read and write the tracked value@count++mutates the value and triggers updates todouble,quadruple, and the DOM
The naming convention is intentional. Earlier versions called this createSignal (like Solid), but people kept trying to use it exactly like Solid’s API and getting confused. By calling it track(), the RippleJS team made it clear: you’re tracking a value, not creating a signal in the Solid sense.
The memory efficiency story
Memory usage doesn’t get talked about nearly as much as it should, so let’s talk about it.
In React, every component instance holds its own state, effects, and rendering logic. Multiply that by hundreds or thousands of components, and the memory overhead becomes significant.
In RippleJS, everything is a “block” with a single relationship between a reactive value and what depends on it. The result is far less overhead per reactive connection.
For large apps with deep trees and many components, that difference in memory usage compounds quickly.
Reactive collections: #[] and #{}
One thing that caught my attention is how RippleJS handles arrays and objects reactively:
export component TodoList() {
const items = #[1, 2, 3]; // TrackedArray
const config = #{theme: 'dark', language: 'en'}; // TrackedObject
<div>
<p>{"Items: "}{items.join(', ')}</p>
<p>{"Theme: "}{config.theme}</p>
<button onClick={() => items.push(items.length + 1)}>
{"Add Item"}
</button>
<button onClick={() => config.theme = config.theme === 'dark' ? 'light' : 'dark'}>
{"Toggle Theme"}
</button>
</div>
}
The #[] and #{} syntax creates fully reactive arrays and objects. When you push to the array or update a property, everything that depends on it updates automatically. There is no need for spread operators; just mutate the data, and the UI updates.
This is a good developer experience improvement over React’s immutable update patterns:
// React - Immutable updates const [items, setItems] = useState([1, 2, 3]); setItems([...items, items.length + 1]); // Create new array // RippleJS - Direct mutation const items = #[1, 2, 3]; items.push(items.length + 1); // Just push
Global state: A deliberate “no”
Now here’s an interesting design decision that might be controversial: RippleJS doesn’t support global state in the traditional sense.
You cannot create tracked values outside components like this:
// This does not work - compilation error
import { track } from 'ripple';
const globalCount = track(0);
// Error: track can only be used within a reactive contect
export component App() {
<div>
<p>{"Global count: "}{@globalCount}</p>
<button onClick={() => @globalCount++}>{"Increment"}</button>
</div>
}
If you do, the code will throw an error of this sort:

The framework enforces that track() must be used within a reactive context (components, functions, or classes created from components). This is an intentional architectural decision.
This is intentional. In RippleJS, everything is about direct relationships between tracked values and the components that use them. There’s no built-in concept of a global store that every component taps into.
If you want a shared state between components, you:
- Make the parent component own the state
- Pass it down via props
- Compose components around that shared owner
Yes, that’s more explicit. But it also sidesteps the usual global state mess that can creep into large React applications.
Component-level styling
RippleJS ships with component-scoped styling built into the syntax:
export component StyledCounter() {
let count = track(0);
<div class="container">
<h1>{"Counter App"}</h1>
<div class="counter-display">
<button class="btn btn-decrement" onClick={() => @count--}>{"-"}</button>
<span class="count-value">{@count}</span>
<button class="btn btn-increment" onClick={() => @count++}>{"+"}</button>
</div>
</div>
<style>
.container {
text-align: center;
font-family: "Arial", sans-serif;
padding: 2rem;
}
.counter-display {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin-top: 1rem;
}
.btn {
height: 3rem;
width: 3rem;
border: none;
border-radius: 0.5rem;
font-size: 1.5rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-decrement {
background-color: #ef4444;
color: white;
}
.btn-decrement:hover {
background-color: #dc2626;
}
.btn-increment {
background-color: #10b981;
color: white;
}
.btn-increment:hover {
background-color: #059669;
}
.count-value {
font-size: 2rem;
font-weight: bold;
min-width: 3rem;
text-align: center;
}
</style>
}
The style shows something like this:

What’s beautiful about this:
- No CSS-in-JS libraries needed – The
<style>tag is a prioritized citizen in your component - Automatic scoping – These styles only apply to this component, no class name collisions
- Co-located with logic – Your styles live right next to the component code that uses them
- Full CSS support – Not a subset or special syntax, just regular CSS with all the features you know
The global CSS “but”
The flip side is global CSS. Right now, you can’t mark styles as global inside components. There’s no global attribute or :global selector yet (though :global is planned).
So you have two options for global styles:
Option 1: Use a <style> tag in your index.html
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Ripple App</title>
<style>
/* Global styles here */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background-color: #f3f4f6;
line-height: 1.6;
}
:root {
--color-primary: #3b82f6;
--color-secondary: #10b981;
--color-danger: #ef4444;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.ts"></script>
</body>
</html>
Many frameworks handle global styles separately from components. In fact, it’s often considered a better practice to keep global styles centralized rather than scattered across components.
The bigger point: having styles co-located with components is a subtle DX win. You’re not constantly hopping between files while you’re in that “build the feature” flow.
Built-in control flow & component system
One of the more refreshing parts of RippleJS is that you write control flow the way you do in plain JavaScript, no .map()gymnastics or nested ternaries when you don’t want them.
Native for loops:
// React - .map()
{users.map(user => (
<div key={user.id}>
<h3>{user.name}</h3>
</div>
))}
// RippleJS - just write a loop
for (const user of props.users) {
<div>
<h3>{user.name}</h3>
</div>
}
No key props, no wrapping JSX in parentheses. Just a loop.
if statements:
// React - nested ternary nightmare
{isLoading ? (
<Spinner />
) : error ? (
<ErrorMessage error={error} />
) : user ? (
<div>Welcome, {user.name}</div>
) : (
<LoginPrompt />
)}
// RippleJS - readable if/else
if (props.isLoading) {
<Spinner />
} else if (props.error) {
<ErrorMessage error={props.error} />
} else if (props.user) {
<div>{"Welcome, " + props.user.name}</div>
} else {
<LoginPrompt />
}
They’re both readable when done well, but for newcomers, Ripple’s control flow often feels more familiar simply because it’s “just JavaScript”.
Try/catch as error boundaries
Instead of separate ErrorBoundary components, you can lean on try/catch:
component SafeComponent(props: { data: any }) {
<div>
try {
<RiskyComponent data={props.data} />
} catch (error) {
<div class="error-message">
{"Something went wrong: " + error.message}
</div>
}
</div>
}
Same mental model you use everywhere else in JavaScript.
Components with props and children
Components work like React, but with cleaner TypeScript-first syntax:
component Button(props: {
text: string
onClick: () => void
variant?: 'primary' | 'danger'
}) {
<button
class={['btn', `btn-${props.variant || 'primary'}`]}
onClick={props.onClick}
>
{props.text}
</button>
<style>
.btn { padding: 0.75rem 1.5rem; border: none; cursor: pointer; }
.btn-primary { background: #3b82f6; color: white; }
.btn-danger { background: #ef4444; color: white; }
</style>
}
// Strongly typed - compiler catches missing/wrong props
export component App() {
<div>
<h1>{"TypeScript Button Test"}</h1>
<h2>{"Strongly Typed Component Props"}</h2>
<div>
<Button text="Primary Button" onClick={() => console.log("Primary clicked")} />
<Button text="Delete" onClick={() => console.log("Deleted")} variant="danger" />
<Button text="Save" onClick={() => alert("Saved!")} variant="primary" />
</div>
</div>
}
Children work the same way:
component Card(props: { title: string, children: Component }) {
<div class="card">
<h2>{props.title}</h2>
<div class="card-body">
<props.children />
</div>
</div>
}
<Card title="Stats">
<p>{"Users: 1,234"}</p>
</Card>
<Card title="Dashboard">
<p>{"Welcome to your dashboard"}</p>
<p>{"You have 5 new notifications"}</p>
</Card>
Again, the key difference is that components don’t return JSX; they are the template. That imperative style gives the compiler more room to optimize.
Control flow comparison
It’s not revolutionary, it’s just sensible.
| Task | React | RippleJS |
|---|---|---|
| Loop | .map() + key |
for loop |
| Condition | Ternary/&& |
if/else |
| Multiple conditions | Nested ternaries | if/else if/else |
| Error handling | ErrorBoundary class | try/catch |
Let’s build something real
Enough theory. Let’s build a Todo app in RippleJS and then compare it with the React version.
Getting started
# Create a new Ripple project npm create ripple my-app # Navigate into the project cd my-app # Install dependencies npm install # Start the dev server npm run dev
Your app should be running at http://localhost:3000
Editor setup
For the best experience, install the RippleJS VS Code extension:
- Open VS Code
- Go to Extensions (
Ctrl+Shift+X/Cmd+Shift+X) - Search for Ripple for VS Code
- Install the official extension

That gives you syntax highlighting, IntelliSense, type checking, and live diagnostics for .ripple files.
The Todo list we’ll build demonstrates most of RippleJS’s core ideas:
- Reactive state with
track() - Native control flow (
if/else,forloops) - Events and form handling
- Conditional rendering and styling
- Component composition
Open App.ripple and drop this in (styles omitted here to keep things focused):
// TodoList.ripple
import { track } from 'ripple';
export component App() {
// Reactive state - these automatically trigger UI updates
const todos = #[]; // TrackedArray for reactive list
let inputValue = track('');
let filter = track('all'); // 'all' | 'active' | 'completed'
let editingId = track(null);
let editValue = track('');
// Derived values - automatically recompute when dependencies change
let filteredTodos = track(() => {
const allTodos = todos;
if (@filter === 'active') {
return allTodos.filter(todo => !todo.completed);
}
if (@filter === 'completed') {
return allTodos.filter(todo => todo.completed);
}
return allTodos;
});
let activeCount = track(() => todos.filter(t => !t.completed).length);
let completedCount = track(() => todos.filter(t => t.completed).length);
// Actions - direct mutations update the UI automatically
const addTodo = () => {
const text = @inputValue.trim();
if (!text) return;
// Just push directly - no setState or immutable patterns needed
todos.push({
id: Date.now(),
text,
completed: false
});
@inputValue = '';
};
const toggleTodo = (id) => {
const todo = todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed; // Direct mutation
}
};
const deleteTodo = (id) => {
const index = todos.findIndex(t => t.id === id);
if (index !== -1) {
todos.splice(index, 1); // Direct mutation
}
};
const startEdit = (todo) => {
@editingId = todo.id;
@editValue = todo.text;
};
const saveEdit = () => {
const todo = todos.find(t => t.id === @editingId);
if (todo && @editValue.trim()) {
todo.text = @editValue.trim();
}
@editingId = null;
@editValue = '';
};
const cancelEdit = () => {
@editingId = null;
@editValue = '';
};
const clearCompleted = () => {
const completed = todos.filter(t => t.completed);
completed.forEach(todo => deleteTodo(todo.id));
};
<div class="todo-app">
<div class="header">
<h1>{"Ripple Todo List"}</h1>
<div class="add-todo">
<input
type="text"
value={@inputValue}
onInput={(e) => @inputValue = e.target.value}
onKeyDown={(e) => e.key === 'Enter' && addTodo()}
placeholder="What needs to be done?"
class="todo-input"
/>
<button onClick={addTodo} class="btn btn-primary">
{"Add"}
</button>
</div>
</div>
{/* Stats bar - derived values update automatically */}
<div class="stats">
<span class="stat">
{"Active: "}<strong>{@activeCount}</strong>
</span>
<span class="stat">
{"Completed: "}<strong>{@completedCount}</strong>
</span>
<span class="stat">
{"Total: "}<strong>{todos.length}</strong>
</span>
</div>
{/* Filter buttons with dynamic classes */}
<div class="filters">
<button
class={@filter === 'all' ? 'filter-btn active' : 'filter-btn'}
onClick={() => @filter = 'all'}
>
{"All"}
</button>
<button
class={@filter === 'active' ? 'filter-btn active' : 'filter-btn'}
onClick={() => @filter = 'active'}
>
{"Active"}
</button>
<button
class={@filter === 'completed' ? 'filter-btn active' : 'filter-btn'}
onClick={() => @filter = 'completed'}
>
{"Completed"}
</button>
</div>
<div class="todo-list">
{/* Native for loop - no .map() needed */}
for (const todo of @filteredTodos) {
<div class={todo.completed ? 'todo-item completed' : 'todo-item'}>
{/* Native if/else - no ternaries */}
if (@editingId === todo.id) {
<input
type="text"
value={@editValue}
onInput={(e) => @editValue = e.target.value}
onKeyDown={(e) => {
if (e.key === 'Enter') saveEdit();
if (e.key === 'Escape') cancelEdit();
}}
class="edit-input"
/>
<div class="edit-actions">
<button onClick={saveEdit} class="btn-icon btn-save">
{"✓"}
</button>
<button onClick={cancelEdit} class="btn-icon btn-cancel">
{"✕"}
</button>
</div>
} else {
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
class="todo-checkbox"
/>
<span
class="todo-text"
onDblClick={() => startEdit(todo)}
>
{todo.text}
</span>
<button
onClick={() => deleteTodo(todo.id)}
class="btn-icon btn-delete"
>
{"
"}
</button>
}
</div>
}
{/* Empty state with conditional message */}
if (@filteredTodos.length === 0) {
<div class="empty-state">
{
@filter === 'completed'
? "No completed todos yet!"
: @filter === 'active'
? "No active todos. Great work!"
: "No todos yet. Add one above!"
}
</div>
}
</div>
{/* Clear button only shows when needed */}
if (@completedCount > 0) {
<div class="footer">
<button onClick={clearCompleted} class="btn btn-secondary">
{"Clear completed (" + @completedCount + ")"}
</button>
</div>
}
</div>
{/* Clone the repo to get the styles */}
}
The interesting parts:
forandifwork directly in the template – no.map()or ternary chains- You mutate data directly (
todos.push,todo.completed = !todo.completed), and the UI just follows filteredTodosrecomputes automatically whentodosorfilterchange – nouseMemo, no dependency arrays- Dynamic classes are handled cleanly; under the hood, Ripple uses something
clsx-like to merge them
This is what our application looks like:

The React equivalent (for comparison)
Here’s the same app in React, conceptually:
function TodoList() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
const [filter, setFilter] = useState('all');
const [editingId, setEditingId] = useState(null);
const [editValue, setEditValue] = useState('');
// All these need useMemo to avoid recalculation on every render
const filteredTodos = useMemo(() => {
if (filter === 'active') return todos.filter(t => !t.completed);
if (filter === 'completed') return todos.filter(t => t.completed);
return todos;
}, [todos, filter]);
const activeCount = useMemo(() =>
todos.filter(t => !t.completed).length, [todos]
);
const completedCount = useMemo(() =>
todos.filter(t => t.completed).length, [todos]
);
// All these need useCallback to avoid recreating on every render
const addTodo = useCallback(() => {
const text = inputValue.trim();
if (!text) return;
setTodos([...todos, {
id: Date.now(),
text,
completed: false
}]);
setInputValue('');
}, [inputValue, todos]);
const toggleTodo = useCallback((id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}, [todos]);
// ... and so on
return (
<div className="todo-app">
{/* JSX with .map() for loops */}
{filteredTodos.map(todo => (
<div key={todo.id}>
{/* nested ternaries for conditions */}
</div>
))}
</div>
);
}
Count it: 5 useState, 3 useMemo, and several useCallback calls if you’re trying to be “proper” about optimization. In Ripple, you mostly just reach for track() and mutate your state without thinking about render cycles.
Fair truth
RippleJS is not production-ready yet, and Dominic is very clear about that. It’s raw, it has bugs, and it’s missing SSR and a mature ecosystem.
So why talk about it at all? Because the ideas are legitimately interesting.
What RippleJS gets right
1. Developer experience
React’s Hooks are powerful, but they also introduce a lot of subtle mental overhead. You constantly think about when to use useCallback, whether useMemo is worth it, and why something keeps re-rendering.
In RippleJS, you track values, access them with @, and let the framework manage dependencies. A lot of that mental tax just disappears.
2. TypeScript that actually fits the framework
Plenty of frameworks “support” TypeScript, but it often feels like an afterthought. RippleJS assumes TypeScript from day one – and designs the syntax, compiler, and DX around it.
3. Performance, you don’t have to baby
When only the DOM nodes that truly depend on a value update, and the framework is more frugal with memory, your app just runs faster without you micromanaging renders. No virtual DOM diffing, no reconciliation passes, no component re-runs.
The bigger question
Is RippleJS going to replace React? Almost certainly not. That’s not an interesting question anyway.
What RippleJS does is explore a different set of trade-offs and ask: What if we made the simple things simple again and the already simple things even simpler?
That’s worth paying attention to.
The framework needs you
RippleJS is open source and very early. Dominic built the first version in a week and now has a few people on board, but turning this into a production-ready framework will take a community.
Areas where help is needed:
- Implementing SSR
- Tooling and DX improvements
- Writing docs and tutorials
- Bug reports and edge-case testing
- Example apps and component libraries
- Contributions to the VS Code extension
- The upcoming
:globalstyle selector
You don’t need to be a compiler engineer. If you’ve ever struggled with a framework’s complexity, you probably have useful instincts about what better looks like.
How to get involved:
- GitHub – https://github.com/trueadm/ripple
- Documentation – https://www.ripplejs.com/docs/introduction
- Twitter – Follow @ripple__js for updates
- Join the Discord
The early contributors to React, Vue, and Svelte helped shape frameworks that millions of developers use today. RippleJS is at that early stage right now. If you’ve ever wanted to be part of building something that could matter, this is your chance.
Conclusion
We’ve taken a pretty deep tour through RippleJS.
I’m not dropping React for production work. But I am watching RippleJS closely, and I’ll absolutely be using it for side projects. The developer experience feels genuinely refreshing, and the performance story is compelling.
Will RippleJS “win”? That depends less on clever technical decisions and more on whether developers adopt it, build things with it, and contribute back.
So here’s the challenge: build something small with RippleJS this weekend – a counter, a to-do list, whatever. See for yourself whether the hype matches reality. And if you hit bugs or missing pieces, don’t just complain on social – open an issue, or a PR.
React’s ecosystem wasn’t built in a day, and it definitely wasn’t built by one person. RippleJS won’t be either.
Now go build something.
The post Ripple over React? Evaluating the newest JS framework appeared first on LogRocket Blog.
















