Handles
Imperative control via handles — scroll controllers and focus nodes
Some Flutter patterns are imperative — you can't express "scroll to offset 500" or "focus this input" as a prop. A handle bridges this gap: a persistent Dart object backing a JS-side reference that you can call methods on and read reactive state from. Two come built in — the scroll controller and the focus node.
ScrollController
createScrollController gives you a ScrollController you can pass to a ScrollView and drive imperatively. It also exposes the scroll offset as a reactive signal.
import { For } from "solid-js";
import {
GestureDetector,
ScrollView,
Text,
View,
createScrollController,
} from "solid-fuse";
function ScrollableList() {
const scroll = createScrollController();
return (
<View flex={{ expand: true }}>
<View flex={{ direction: "horizontal", gap: 8 }} padding={8}>
<GestureDetector onTap={() => scroll.animateTo(0, { duration: 300 })}>
<View padding={8} decoration={{ color: "blue", borderRadius: 4 }}>
<Text color="white">Top</Text>
</View>
</GestureDetector>
<Text>Offset: {Math.round(scroll.scrollOffset())}</Text>
</View>
<ScrollView controller={scroll} flex={{ expand: true }}>
<View flex={{ gap: 4 }} padding={16}>
<For each={Array.from({ length: 100 }, (_, i) => i)}>
{(i) => <Text>Item {i}</Text>}
</For>
</View>
</ScrollView>
</View>
);
}API
| Method | Description |
|---|---|
scroll.jumpTo(offset) | Jump to position immediately |
scroll.animateTo(offset, { duration? }) | Smooth scroll to position (duration in ms, default 300) |
scroll.scrollOffset() | Reactive signal — current scroll position, updates as the user scrolls |
scroll.dispose() | Dispose the underlying Dart object. Automatic if created inside a reactive owner. |
Initial offset
const scroll = createScrollController({ initialScrollOffset: 200 });FocusNode
createFocusNode gives you a Flutter FocusNode you can pass to a TextField and drive imperatively. It exposes hasFocus as a reactive signal.
import { TextField, Text, View, createFocusNode } from "solid-fuse";
function FocusDemo() {
const focus = createFocusNode();
return (
<View flex={{ gap: 6 }}>
<TextField focusNode={focus} placeholder="Type here" />
<Text>hasFocus = {focus.hasFocus() ? "true" : "false"}</Text>
<GestureDetector onTap={() => focus.focus()}>
<Text color="blue">Focus the field</Text>
</GestureDetector>
<GestureDetector onTap={() => focus.unfocus()}>
<Text color="blue">Unfocus</Text>
</GestureDetector>
</View>
);
}API
| Method | Description |
|---|---|
focus.focus() | Request focus |
focus.unfocus() | Drop focus |
focus.hasFocus() | Reactive signal — true while the field is focused |
focus.dispose() | Dispose. Automatic if created inside a reactive owner. |
How they work
Both are thin wrappers around the same handle system:
createHandle("scrollController", { initialScrollOffset, setScrollOffset })creates a JS-side{ node, call, dispose }triple. The Dart side instantiates aFuseScrollControllerhandle wrapping a real FlutterScrollController.- When you pass
controller={scroll}to<ScrollView>, the renderer serializes the reference as_node: <id>. The DartScrollViewwidget reads it as aFuseNodereference, looks up the handle, and uses its wrappedScrollController. - State updates from Dart (the user scrolls) call the
setScrollOffsetcallback you passed in, which updates a Solid signal. - Imperative calls (
scroll.animateTo(...)) go over a_handleCallchannel that targets the node by id.
The signal updates are not lazy — every Dart-side change calls into JS. Use Math.round or wrap reads in your own createMemo if you want to throttle re-renders.
Building your own
You can create handles for any persistent native Dart object — animation controllers, page route observers, native plugin instances. See Creating handles for the full pattern.