dart_node_react

类型安全的 React 绑定,用于在 Dart 中构建 Web 应用程序。如果您熟悉 React,您会感到非常亲切。

安装

dependencies:
  dart_node_react: ^0.13.0-beta

通过 npm 安装 React:

npm install react react-dom

快速开始

import 'dart:js_interop';

import 'package:dart_node_react/dart_node_react.dart';

ReactElement app() {
  return div(
    className: 'app',
    children: [
      h1('Hello, Dart!'),
      pEl('Welcome to React with Dart.'),
    ],
  );
}

void main() {
  final container = Document.getElementById('root');
  if (container case final JSObject c) {
    createRoot(c).render(app());
  }
}

组件

函数组件

ReactElement greeting({required String name}) {
  return div(
    className: 'greeting',
    children: [
      pEl('Hello, $name!'),
    ],
  );
}

// 使用方式
greeting(name: 'World');

带 Props 的组件

ReactElement userCard({
  required String name,
  required String email,
  String? avatarUrl,
}) {
  return div(
    className: 'user-card',
    children: [
      avatarUrl != null
          ? img(src: avatarUrl, alt: name)
          : div(className: 'avatar-placeholder'),
      h2(name),
      pEl(email),
    ],
  );
}

Hooks

useState

返回包含 .value.set().setWithUpdater()StateHook<T>

ReactElement counter() {
  final count = useState(0);

  return div(children: [
    pEl('Count: ${count.value}'),
    button(
      text: 'Increment',
      onClick: () => count.setWithUpdater((c) => c + 1),
    ),
    button(
      text: 'Decrement',
      onClick: () => count.setWithUpdater((c) => c - 1),
    ),
  ]);
}

useStateLazy

用于昂贵的初始状态计算:

final data = useStateLazy(() => expensiveComputation());

useEffect

ReactElement timer() {
  final seconds = useState(0);

  useEffect(() {
    final timer = Timer.periodic(Duration(seconds: 1), (_) {
      seconds.setWithUpdater((s) => s + 1);
    });

    // 清理函数
    return () => timer.cancel();
  }, []); // 空依赖数组 = 仅在挂载时运行一次

  return pEl('Seconds: ${seconds.value}');
}

useLayoutEffect

useEffect 的同步版本,在屏幕更新前运行:

useLayoutEffect(() {
  // DOM 测量
  return () { /* 清理 */ };
}, [dependency]);

useRef

ReactElement focusInput() {
  final inputRef = useRef<JSObject>(null);

  void handleClick() {
    if (inputRef.jsRef.current case final JSObject node) {
      node.callMethod('focus'.toJS);
    }
  }

  return div(children: [
    input(type: 'text', props: {'ref': inputRef.jsRef}),
    button(
      text: 'Focus Input',
      onClick: handleClick,
    ),
  ]);
}

useMemo

ReactElement expensiveList({required List<int> numbers}) {
  final count = useState(0);

  // 仅当 count.value 变化时重新计算
  final fib = useMemo(
    () => fibonacci(count.value),
    [count.value],
  );

  return div(children: [
    pEl('Fibonacci of ${count.value} is $fib'),
  ]);
}

useCallback

ReactElement searchBox({required void Function(String) onSearch}) {
  final query = useState('');

  // 记忆化回调
  final handleSubmit = useCallback(
    () => onSearch(query.value),
    [query.value, onSearch],
  );

  return form(null, [
    input(
      value: query.value,
      onChange: (e) {
        if (e.target case final JSObject t) {
          if (t['value'] case final JSString s) query.set(s.toDart);
        }
      },
    ),
    button(text: 'Search', props: {'type': 'submit', 'onClick': handleSubmit}),
  ]);
}

useDebugValue

在 React DevTools 中显示自定义标签:

useDebugValue<bool>(
  isOnline.value,
  (isOnline) => isOnline ? 'Online' : 'Not Online',
);

元素

HTML 元素

// Div 和 span
div(className: 'container', children: [...])
span('highlight text', className: 'highlight')

// 标题
h1('Title')
h2('Subtitle')

// 段落和文本
pEl('Some text')

// 链接
a(href: 'https://example.com', text: 'Click me')

// 图片
img(src: '/image.png', alt: 'Description')

// 表单
form(null, [...])
input(type: 'text', value: value, onChange: handleChange)
button(text: 'Submit', props: {'type': 'submit'})

列表

ReactElement todoList({required List<Todo> todos}) {
  return ul(
    className: 'todo-list',
    children: todos
        .map(
          (todo) => li(
            todo.title,
            props: {'key': todo.id},
          ),
        )
        .toList(),
  );
}

条件渲染

ReactElement userStatus({required User? user}) {
  return div(children: [
    user != null
        ? span('Welcome, ${user.name}!')
        : span('Please log in'),
  ]);
}

事件处理

ReactElement interactiveButton() {
  void handleClick() {
    print('Button clicked');
  }

  return button(
    text: 'Hover and Click Me',
    onClick: handleClick,
  );
}

表单事件

ReactElement loginForm() {
  final email = useState('');
  final password = useState('');

  void handleSubmit(SyntheticEvent e) {
    e.preventDefault();
    print('Login: ${email.value} / ${password.value}');
  }

  String readValue(SyntheticEvent e) => switch (e.target) {
    final JSObject t => switch (t['value']) {
      final JSString s => s.toDart,
      _ => '',
    },
    _ => '',
  };

  return form({'onSubmit': handleSubmit}, [
    input(
      type: 'email',
      value: email.value,
      onChange: (e) => email.set(readValue(e)),
      placeholder: 'Email',
    ),
    input(
      type: 'password',
      value: password.value,
      onChange: (e) => password.set(readValue(e)),
      placeholder: 'Password',
    ),
    button(text: 'Log In', props: {'type': 'submit'}),
  ]);
}

样式

内联样式

div(
  style: {
    'backgroundColor': '#f0f0f0',
    'padding': '1rem',
    'borderRadius': '8px',
  },
  children: [...],
)

CSS 类

div(
  className: 'card card-primary',
  children: [...],
)

完整示例

import 'dart:js_interop';
import 'dart:js_interop_unsafe';

import 'package:dart_node_react/dart_node_react.dart';

ReactElement todoApp() {
  final todos = useState<List<Todo>>([]);
  final text = useState('');

  void addTodo() {
    if (text.value.trim().isEmpty) return;

    todos.setWithUpdater((prev) => [
      ...prev,
      Todo(id: DateTime.now().toString(), title: text.value, completed: false),
    ]);
    text.set('');
  }

  void toggleTodo(String id) {
    todos.setWithUpdater((prev) => prev.map((todo) =>
      todo.id == id
          ? Todo(id: todo.id, title: todo.title, completed: !todo.completed)
          : todo
    ).toList());
  }

  return div(
    className: 'todo-app',
    children: [
      h1('Todo List'),

      form({
        'onSubmit': (SyntheticEvent e) {
          e.preventDefault();
          addTodo();
        },
      }, [
        input(
          value: text.value,
          onChange: (e) {
            if (e.target case final JSObject t) {
              if (t['value'] case final JSString s) text.set(s.toDart);
            }
          },
          placeholder: 'What needs to be done?',
        ),
        button(text: 'Add', props: {'type': 'submit'}),
      ]),

      ul(
        children: todos.value
            .map(
              (todo) => li(
                todo.title,
                className: todo.completed ? 'completed' : '',
                props: {
                  'key': todo.id,
                  'onClick': () => toggleTodo(todo.id),
                },
              ),
            )
            .toList(),
      ),

      pEl('${todos.value.where((t) => !t.completed).length} items left'),
    ],
  );
}

class Todo {
  final String id;
  final String title;
  final bool completed;

  Todo({required this.id, required this.title, required this.completed});
}

void main() {
  final container = Document.getElementById('root');
  if (container case final JSObject c) {
    createRoot(c).render(todoApp());
  }
}

源代码

源代码可在 GitHub 上获取。