Practical patterns and common pitfalls when building applications with JavaScriptKit.
There are two primary ways to architect a WebAssembly application with JavaScriptKit, depending on how much of the web ecosystem you want to leverage.
In this approach, Swift handles everything: DOM manipulation, state management, and application logic.
- How it works: Swift uses JavaScriptKit to call DOM APIs directly (e.g.,
document.createElement,addEventListener). - Declarative Alternative: Instead of raw DOM manipulation, you can use declarative UI libraries like Elementary, which provides a more Swift-native way to build HTML components.
- Best for: Small to medium applications, developers who prefer staying entirely within Swift, or when porting existing Swift logic that heavily controls its own UI.
- Data Flow: Swift → JavaScript (DOM APIs).
In this approach, modern web frameworks (like React, Vue, or Svelte) handle the UI and state management, while Swift provides the heavy-duty business logic.
- How it works: Swift exposes specific functions or objects to the JavaScript
globalobject. The JavaScript UI then calls into these Swift APIs. - Best for: Applications requiring complex UIs, leveraging existing web components/libraries, or when Swift is only needed for specialized tasks (e.g., data processing, cryptography, shared logic with a mobile app).
- Data Flow: JavaScript (UI) → Swift (Core Logic).
// Swift side
let api = JSObject()
api.processData = JSClosure { args in
let input = args[0].string ?? ""
let result = HeavyLogic.process(input)
return .string(result)
}
JSObject.global.mySwiftApp = .object(api)// JavaScript side (e.g., in a React component)
const result = window.mySwiftApp.processData("input data");JavaScript does not participate in Swift's Automatic Reference Counting (ARC). You must manually manage the lifetime of Swift objects and closures that are exposed to JavaScript.
JSClosure instances must be retained by the Swift side as long as they are expected to be called by JavaScript. If a JSClosure is deallocated, any attempt by JavaScript to call it will result in a crash.
When using [weak self] in a closure, ensure the capturing object is also retained.
class UIManager {
private var clickHandler: JSClosure?
private let button: JSObject
init(button: JSObject) {
self.button = button
// clickHandler must be stored as a property to be retained
self.clickHandler = JSClosure { [weak self] _ in
self?.handleClick()
return .undefined
}
_ = button.addEventListener!("click", clickHandler!)
}
func handleClick() {
// ...
}
}When an object owning an event listener is deallocated, it is recommended to remove the event listener from the DOM to avoid memory leaks or attempts to call deallocated closures.
class UIManager {
private var clickHandler: JSClosure?
private let button: JSObject
// ... init ...
deinit {
if let handler = clickHandler {
_ = button.removeEventListener!("click", handler)
}
}
}For root-level UI managers or state containers that should live for the duration of the application, store them in a static property in your @main entry point.
@main
struct MyApp {
static nonisolated(unsafe) var ui: UIManager!
static func main() {
let ui = UIManager()
ui.setup()
Self.ui = ui // Retain for application lifetime
}
}let input = document.getElementById!("text-input").object!
let closure = JSClosure { [weak self] args in
guard let event = args.first?.object,
event.key.string == "Enter" else { return .undefined }
self?.handleSubmit()
return .undefined
}
_ = input.addEventListener!("keydown", closure)let checkbox = document.getElementById!("toggle").object!
let closure = JSClosure { [weak self] _ in
let isChecked = checkbox.checked.boolean ?? false
self?.updateToggleState(isChecked)
return .undefined
}
_ = checkbox.addEventListener!("change", closure)Values must be converted to JSValue using .jsValue or explicit wrappers like .string().
let text = "Hello"
// Correct
element.textContent = .string(text)
element.textContent = text.jsValue
// Incorrect
element.textContent = textEnsure you use ! only for method calls that return an optional closure via Dynamic Member Lookup.
// Method call (requires !)
_ = document.createElement!("div")
// Property access (no !)
let body = document.body.object!Many JavaScript methods return a value that Swift requires you to handle. Use _ = to silence warnings.
_ = document.body.object!.appendChild!(element)