What if you could build React apps without the existential dread of undefined is not a function? What if your types actually meant something at runtime? What if you never had to debug another Cannot read property 'map' of null error at 2 AM?
Good news: you can. With dart_node_react, you write React applications entirely in Dart. Same React patterns you know. Real type safety you've been dreaming about.
Why Dart? (Besides the Obvious Joy of Not Using JavaScript)
Let's be honest. TypeScript was a massive improvement over JavaScript. But its types are like a bouncer who checks IDs at the door and then goes home. Once you're past the compiler, anything goes.
Dart takes a different approach. Types exist at runtime. Null safety is sound. When your code compiles, you know your String is actually a String and not secretly undefined wearing a fake mustache.
Already know Flutter? You already know Dart. Now you can use those same skills to build React web apps. One language. Full stack. No context switching between "Dart brain" and "TypeScript brain."
Setting Up Your Project
Getting started takes about 30 seconds. Create a new Dart project:
mkdir my_react_app && cd my_react_app
dart create -t package .
Add the dependencies to your pubspec.yaml:
name: my_react_app
environment:
sdk: ^3.0.0
dependencies:
dart_node_core: ^0.11.0-beta
dart_node_react: ^0.11.0-beta
Run dart pub get. Done. No webpack config. No babel. No 47 dev dependencies fighting each other.
Your First Component
Create web/app.dart. This is where the magic happens:
import 'dart:js_interop';
import 'package:dart_node_react/dart_node_react.dart';
void main() {
final root = Document.getElementById('root');
(root != null)
? ReactDOM.createRoot(root).render(App())
: throw StateError('Root element not found');
}
ReactElement App() => createElement(
((JSAny props) {
return div(
className: 'app',
children: [
h1('Hello from Dart!'),
pEl('Look ma, no JavaScript!'),
],
);
}).toJS,
);
The createElement function wraps your component logic. Inside, you return React elements using helper functions like div, h1, and pEl. It feels like React because it is React, just with better types.
State Management: useState Without the Guesswork
Here's where Dart really shines. The useState hook returns a StateHook<T> with actual, honest-to-goodness type safety:
ReactElement Counter() => createElement(
((JSAny props) {
final count = useState(0);
return div(
className: 'counter',
children: [
h2('Count: ${count.value}'),
button(
text: 'Increment',
onClick: (_) => count.setWithUpdater((c) => c + 1),
),
button(
text: 'Reset',
onClick: (_) => count.set(0),
),
],
);
}).toJS,
);
Three ways to update state:
count.value- read the current valuecount.set(5)- set a new value directlycount.setWithUpdater((old) => old + 1)- update based on previous value
No more useState<number | undefined>(undefined) gymnastics. Just useState(0). The compiler knows it's an int.
Building Forms (The Part Everyone Dreads)
Forms don't have to be painful. Here's a login form that actually works:
ReactElement LoginForm() => createElement(
((JSAny props) {
final emailState = useState('');
final passwordState = useState('');
final errorState = useState<String?>(null);
void handleSubmit() {
if (emailState.value.isEmpty || passwordState.value.isEmpty) {
errorState.set('Please fill in all fields');
return;
}
print('Logging in: ${emailState.value}');
}
return div(
className: 'login-form',
children: [
h2('Sign In'),
if (errorState.value != null)
div(className: 'error', child: span(errorState.value!)),
input(
type: 'email',
placeholder: 'Email',
value: emailState.value,
className: 'input',
onChange: (e) => emailState.set(getInputValue(e).toDart),
),
input(
type: 'password',
placeholder: 'Password',
value: passwordState.value,
className: 'input',
onChange: (e) => passwordState.set(getInputValue(e).toDart),
),
button(
text: 'Sign In',
className: 'btn btn-primary',
onClick: handleSubmit,
),
],
);
}).toJS,
);
The getInputValue helper extracts input values from events. Call .toDart to convert JavaScript strings to Dart strings. Clean and predictable.
Side Effects with useEffect
Need to fetch data when a component mounts? useEffect works exactly like you'd expect:
ReactElement UserList() => createElement(
((JSAny props) {
final usersState = useState<List<String>>([]);
final loadingState = useState(true);
useEffect(() {
Future<void> loadUsers() async {
await Future.delayed(Duration(seconds: 1));
usersState.set(['Alice', 'Bob', 'Charlie']);
loadingState.set(false);
}
unawaited(loadUsers());
return null;
}, []);
return div(
className: 'user-list',
children: [
h2('Users'),
if (loadingState.value)
span('Loading...')
else
ul(
children: usersState.value
.map((user) => li(child: span(user)))
.toList(),
),
],
);
}).toJS,
);
Pass an empty list [] to run the effect only on mount. Return a cleanup function or null if you don't need cleanup. No surprises here.
All Your Favorite HTML Elements
dart_node_react provides functions for every HTML element you need:
ReactElement PageLayout() => createElement(
((JSAny props) {
return div(
className: 'layout',
children: [
header(
className: 'header',
child: h1('My Dart React App'),
),
mainEl(
className: 'main-content',
children: [
section(
className: 'hero',
children: [
h2('Welcome'),
pEl('Build type-safe React apps with Dart.'),
],
),
],
),
footer(
className: 'footer',
child: pEl('Built with dart_node_react'),
),
],
);
}).toJS,
);
You get div, span, h1-h6, pEl, ul, li, button, input, form, header, footer, mainEl, section, nav, article, and more. Everything you need to build real UIs.
Compiling and Running
Create web/index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My Dart React App</title>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="app.dart.js"></script>
</body>
</html>
Compile your Dart to JavaScript:
dart compile js web/app.dart -o web/app.dart.js
Serve the web directory and open it in your browser. That's it. Your React app is running, and you didn't write a single line of JavaScript.
Putting It Together: A Task Manager
Here's a complete example combining everything you've learned:
ReactElement TaskManager() => createElement(
((JSAny props) {
final tasksState = useState<List<String>>([]);
final newTaskState = useState('');
void addTask() {
switch (newTaskState.value.trim().isEmpty) {
case true:
return;
case false:
tasksState.setWithUpdater(
(tasks) => [...tasks, newTaskState.value],
);
newTaskState.set('');
}
}
void removeTask(int index) {
tasksState.setWithUpdater((tasks) {
final updated = [...tasks];
updated.removeAt(index);
return updated;
});
}
return div(
className: 'task-manager',
children: [
h2('My Tasks'),
div(
className: 'add-task',
children: [
input(
type: 'text',
placeholder: 'New task...',
value: newTaskState.value,
onChange: (e) => newTaskState.set(getInputValue(e).toDart),
),
button(text: 'Add', onClick: addTask),
],
),
ul(
className: 'task-list',
children: tasksState.value.indexed
.map(
(item) => li(
children: [
span(item.$2),
button(
text: 'Delete',
onClick: (_) => removeTask(item.$1),
),
],
),
)
.toList(),
),
],
);
}).toJS,
);
State management, event handling, list rendering. All type-safe. All Dart.
What's Next?
You've got the basics. Now go build something. Explore more hooks like useMemo and useCallback. Check out the full-stack example with authentication, API integration, and WebSocket support.
No more fighting with type coercion. No more any escape hatches. Just clean, type-safe React apps in a language that respects your time.
Welcome to the future. It compiles to JavaScript, but at least you don't have to write it.