diff --git a/Cargo.toml b/Cargo.toml
index 0eab610..0f22189 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,6 +27,7 @@ lazy_static = "1"
version = "0.3"
features = [
"CssStyleDeclaration",
+ "CloseEvent",
"CustomEvent",
"CustomEventInit",
"DomRectReadOnly",
@@ -62,6 +63,7 @@ features = [
docs = []
math = ["num"]
storage = ["serde", "serde_json", "web-sys/StorageEvent"]
+websocket = ["web-sys/BinaryType", "web-sys/WebSocket"]
[package.metadata.docs.rs]
all-features = true
diff --git a/examples/Cargo.toml b/examples/Cargo.toml
index 5ec52d0..163c8e4 100644
--- a/examples/Cargo.toml
+++ b/examples/Cargo.toml
@@ -28,6 +28,7 @@ members = [
"use_scroll",
"use_storage",
"use_throttle_fn",
+ "use_websocket",
"watch_debounced",
"watch_pausable",
"watch_throttled",
diff --git a/examples/use_websocket/Cargo.toml b/examples/use_websocket/Cargo.toml
new file mode 100644
index 0000000..0b2c27b
--- /dev/null
+++ b/examples/use_websocket/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "use_websocket"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+leptos = { version = "0.4", features = ["nightly", "csr"] }
+console_error_panic_hook = "0.1"
+console_log = "1"
+log = "0.4"
+leptos-use = { path = "../..", features = ["docs", "websocket"] }
+web-sys = "0.3"
+
+[dev-dependencies]
+wasm-bindgen = "0.2"
+wasm-bindgen-test = "0.3.0"
diff --git a/examples/use_websocket/README.md b/examples/use_websocket/README.md
new file mode 100644
index 0000000..7030fa4
--- /dev/null
+++ b/examples/use_websocket/README.md
@@ -0,0 +1,22 @@
+A simple example for `use_websocket`.
+
+If you don't have it installed already, install [Trunk](https://trunkrs.dev/) and [Tailwind](https://tailwindcss.com/docs/installation)
+as well as the nightly toolchain for Rust and the wasm32-unknown-unknown target:
+
+```bash
+cargo install trunk
+rustup toolchain install nightly
+rustup target add wasm32-unknown-unknown
+```
+
+Then, open two terminals. In the first one, run:
+
+```
+npx tailwindcss -i ./input.css -o ./style/output.css --watch
+```
+
+In the second one, run:
+
+```bash
+trunk serve --open
+```
diff --git a/examples/use_websocket/Trunk.toml b/examples/use_websocket/Trunk.toml
new file mode 100644
index 0000000..3e4be08
--- /dev/null
+++ b/examples/use_websocket/Trunk.toml
@@ -0,0 +1,2 @@
+[build]
+public_url = "/demo/"
\ No newline at end of file
diff --git a/examples/use_websocket/index.html b/examples/use_websocket/index.html
new file mode 100644
index 0000000..ae249a6
--- /dev/null
+++ b/examples/use_websocket/index.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/examples/use_websocket/input.css b/examples/use_websocket/input.css
new file mode 100644
index 0000000..bd6213e
--- /dev/null
+++ b/examples/use_websocket/input.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
\ No newline at end of file
diff --git a/examples/use_websocket/rust-toolchain.toml b/examples/use_websocket/rust-toolchain.toml
new file mode 100644
index 0000000..5d56faf
--- /dev/null
+++ b/examples/use_websocket/rust-toolchain.toml
@@ -0,0 +1,2 @@
+[toolchain]
+channel = "nightly"
diff --git a/examples/use_websocket/src/main.rs b/examples/use_websocket/src/main.rs
new file mode 100644
index 0000000..54a87f2
--- /dev/null
+++ b/examples/use_websocket/src/main.rs
@@ -0,0 +1,203 @@
+use leptos::*;
+use leptos_use::docs::demo_or_body;
+use leptos_use::websocket::*;
+
+#[component]
+fn Demo(cx: Scope) -> impl IntoView {
+ let (history, set_history) = create_signal(cx, vec![]);
+
+ fn update_history(&history: &WriteSignal>, message: String) {
+ let _ = &history.update(|history: &mut Vec<_>| history.push(message));
+ }
+ // ----------------------------
+ // use_websocket
+ // ----------------------------
+
+ let UseWebsocketReturn {
+ ready_state,
+ message,
+ message_bytes,
+ send,
+ send_bytes,
+ open,
+ close,
+ ..
+ } = use_websocket(cx, "wss://echo.websocket.events/".to_string());
+
+ let send_message = move |_| {
+ let m = "Hello, world!".to_string();
+ send(m.clone());
+ set_history.update(|history: &mut Vec<_>| history.push(format! {"[send]: {:?}", m}));
+ };
+
+ let send_byte_message = move |_| {
+ let m = b"Hello, world!\r\n".to_vec();
+ send_bytes(m.clone());
+ set_history.update(|history: &mut Vec<_>| history.push(format! {"[send_bytes]: {:?}", m}));
+ };
+
+ let status = move || ready_state().to_string();
+
+ let connected = move || ready_state.get() == UseWebSocketReadyState::Open;
+
+ let open_connection = move |_| {
+ open();
+ };
+ let close_connection = move |_| {
+ close();
+ };
+
+ create_effect(cx, move |_| {
+ if let Some(m) = message.get() {
+ update_history(&set_history, format! {"[message]: {:?}", m});
+ };
+ });
+
+ create_effect(cx, move |_| {
+ if let Some(m) = message_bytes.get() {
+ update_history(&set_history, format! {"[message_bytes]: {:?}", m});
+ };
+ });
+
+ // ----------------------------
+ // use_websocket_with_options
+ // ----------------------------
+
+ let (history2, set_history2) = create_signal(cx, vec![]);
+
+ let UseWebsocketReturn {
+ ready_state: ready_state2,
+ send: send2,
+ send_bytes: send_bytes2,
+ open: open2,
+ close: close2,
+ message: message2,
+ message_bytes: message_bytes2,
+ ..
+ } = use_websocket_with_options(
+ cx,
+ "wss://echo.websocket.events/".to_string(),
+ UseWebSocketOptions {
+ manual: true,
+ onopen: Some(Box::new(move |e| {
+ set_history2.update(|history: &mut Vec<_>| {
+ history.push(format! {"[onopen]: event {:?}", e.type_()})
+ });
+ })),
+ onclose: Some(Box::new(move |e| {
+ set_history2.update(|history: &mut Vec<_>| {
+ history.push(format! {"[onclose]: event {:?}", e.type_()})
+ });
+ })),
+ onerror: Some(Box::new(move |e| {
+ set_history2.update(|history: &mut Vec<_>| {
+ history.push(format! {"[onerror]: event {:?}", e.type_()})
+ });
+ })),
+ onmessage: Some(Box::new(move |m| {
+ set_history2
+ .update(|history: &mut Vec<_>| history.push(format! {"[onmessage]: {:?}", m}));
+ })),
+ onmessage_bytes: Some(Box::new(move |m| {
+ set_history2.update(|history: &mut Vec<_>| {
+ history.push(format! {"[onmessage_bytes]: {:?}", m})
+ });
+ })),
+ ..Default::default()
+ },
+ );
+
+ let open_connection2 = move |_| {
+ open2();
+ };
+ let close_connection2 = move |_| {
+ close2();
+ };
+
+ let send_message2 = move |_| {
+ let message = "Hello, use_leptos!".to_string();
+ send2(message.clone());
+ update_history(&set_history2, format! {"[send]: {:?}", message});
+ };
+
+ let send_byte_message2 = move |_| {
+ let m = b"Hello, world!\r\n".to_vec();
+ send_bytes2(m.clone());
+ update_history(&set_history2, format! {"[send_bytes]: {:?}", m});
+ };
+
+ let status2 = move || ready_state2.get().to_string();
+
+ create_effect(cx, move |_| {
+ if let Some(m) = message2.get() {
+ update_history(&set_history2, format! {"[message]: {:?}", m});
+ };
+ });
+
+ create_effect(cx, move |_| {
+ if let Some(m) = message_bytes2.get() {
+ update_history(&set_history2, format! {"[message_bytes]: {:?}", m});
+ };
+ });
+
+ let connected2 = move || ready_state2.get() == UseWebSocketReadyState::Open;
+
+ view! { cx,
+
+
+
+
"use_websocket"
+
"status: " {status}
+
+
+
+
+
+
"History"
+
+
+
{message} }
+ }
+ />
+
+
+
+
"use_websocket_with_options"
+
"status: " {status2}
+
+
+
+
+
+
"History"
+
+
+
+
+
+
+
+
+ }
+}
+
+fn main() {
+ _ = console_log::init_with_level(log::Level::Info);
+ console_error_panic_hook::set_once();
+
+ mount_to(demo_or_body(), |cx| {
+ view! { cx, }
+ })
+}
diff --git a/examples/use_websocket/style/output.css b/examples/use_websocket/style/output.css
new file mode 100644
index 0000000..a9427f9
--- /dev/null
+++ b/examples/use_websocket/style/output.css
@@ -0,0 +1,214 @@
+*, ::before, ::after {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia:
+}
+
+::backdrop {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia:
+}
+
+.container {
+ width: 100%
+}
+
+@media (min-width: 640px) {
+ .container {
+ max-width: 640px
+ }
+}
+
+@media (min-width: 768px) {
+ .container {
+ max-width: 768px
+ }
+}
+
+@media (min-width: 1024px) {
+ .container {
+ max-width: 1024px
+ }
+}
+
+@media (min-width: 1280px) {
+ .container {
+ max-width: 1280px
+ }
+}
+
+@media (min-width: 1536px) {
+ .container {
+ max-width: 1536px
+ }
+}
+
+.static {
+ position: static
+}
+
+.mb-2 {
+ margin-bottom: 0.5rem
+}
+
+.mr-2 {
+ margin-right: 0.5rem
+}
+
+.flex {
+ display: flex
+}
+
+.w-full {
+ width: 100%
+}
+
+.flex-col {
+ flex-direction: column
+}
+
+.items-center {
+ align-items: center
+}
+
+.gap-4 {
+ gap: 1rem
+}
+
+.p-0 {
+ padding: 0px
+}
+
+.text-2xl {
+ font-size: 1.5rem;
+ line-height: 2rem
+}
+
+.text-xl {
+ font-size: 1.25rem;
+ line-height: 1.75rem
+}
+
+.text-\[--brand-color\] {
+ color: var(--brand-color)
+}
+
+.text-green-600 {
+ --tw-text-opacity: 1;
+ color: rgb(22 163 74 / var(--tw-text-opacity))
+}
+
+.opacity-75 {
+ opacity: 0.75
+}
+
+@media (prefers-color-scheme: dark) {
+ .dark\:text-green-500 {
+ --tw-text-opacity: 1;
+ color: rgb(34 197 94 / var(--tw-text-opacity))
+ }
+}
+
+@media (min-width: 1024px) {
+ .lg\:w-1\/2 {
+ width: 50%
+ }
+
+ .lg\:flex-row {
+ flex-direction: row
+ }
+
+ .lg\:text-4xl {
+ font-size: 2.25rem;
+ line-height: 2.5rem
+ }
+}
\ No newline at end of file
diff --git a/examples/use_websocket/tailwind.config.js b/examples/use_websocket/tailwind.config.js
new file mode 100644
index 0000000..c5f4159
--- /dev/null
+++ b/examples/use_websocket/tailwind.config.js
@@ -0,0 +1,13 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: {
+ files: ["*.html", "./src/**/*.rs", "../../src/docs/**/*.rs"],
+ },
+ theme: {
+ extend: {},
+ },
+ corePlugins: {
+ preflight: false,
+ },
+ plugins: [],
+}
diff --git a/src/lib.rs b/src/lib.rs
index 9d2d5d0..987a01d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -9,6 +9,8 @@ pub mod math;
#[cfg(feature = "storage")]
pub mod storage;
pub mod utils;
+#[cfg(feature = "websocket")]
+pub mod websocket;
#[cfg(web_sys_unstable_apis)]
mod use_element_size;
diff --git a/src/websocket/mod.rs b/src/websocket/mod.rs
new file mode 100644
index 0000000..ef6e33b
--- /dev/null
+++ b/src/websocket/mod.rs
@@ -0,0 +1,3 @@
+mod use_websocket;
+
+pub use use_websocket::*;
diff --git a/src/websocket/use_websocket.rs b/src/websocket/use_websocket.rs
new file mode 100644
index 0000000..51715a6
--- /dev/null
+++ b/src/websocket/use_websocket.rs
@@ -0,0 +1,406 @@
+use leptos::{leptos_dom::helpers::TimeoutHandle, *};
+
+use core::fmt;
+use std::rc::Rc;
+use std::{cell::RefCell, time::Duration};
+
+use js_sys::Array;
+use wasm_bindgen::{prelude::*, JsCast, JsValue};
+use web_sys::{BinaryType, Event, MessageEvent, WebSocket};
+
+pub use web_sys::CloseEvent;
+
+use crate::utils::CloneableFnMutWithArg;
+
+/// The current state of the `WebSocket` connection.
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum UseWebSocketReadyState {
+ Connecting,
+ Open,
+ Closing,
+ Closed,
+}
+
+impl fmt::Display for UseWebSocketReadyState {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match *self {
+ UseWebSocketReadyState::Connecting => write!(f, "Connecting"),
+ UseWebSocketReadyState::Open => write!(f, "Open"),
+ UseWebSocketReadyState::Closing => write!(f, "Closing"),
+ UseWebSocketReadyState::Closed => write!(f, "Closed"),
+ }
+ }
+}
+
+/// Options for `WebSocket`.
+// #[derive(DefaultBuilder)]
+#[derive(Clone)]
+pub struct UseWebSocketOptions {
+ /// `WebSocket` connect callback.
+ pub onopen: Option>>,
+ /// `WebSocket` message callback for text.
+ pub onmessage: Option>>,
+ /// `WebSocket` message callback for binary.
+ pub onmessage_bytes: Option>>>,
+ /// `WebSocket` error callback.
+ pub onerror: Option>>,
+ /// `WebSocket` close callback.
+ pub onclose: Option>>,
+
+ /// Retry times.
+ pub reconnect_limit: Option,
+ /// Retry interval(ms).
+ pub reconnect_interval: Option,
+ /// Manually starts connection
+ pub manual: bool,
+ /// Sub protocols
+ pub protocols: Option>,
+}
+
+impl Default for UseWebSocketOptions {
+ fn default() -> Self {
+ Self {
+ onopen: None,
+ onmessage: None,
+ onmessage_bytes: None,
+ onerror: None,
+ onclose: None,
+ reconnect_limit: Some(3),
+ reconnect_interval: Some(3 * 1000),
+ manual: false,
+ protocols: Default::default(),
+ }
+ }
+}
+
+/// Return type of [`use_websocket`].
+#[derive(Clone)]
+pub struct UseWebsocketReturn
+where
+ OpenFn: Fn() + Clone + 'static,
+ CloseFn: Fn() + Clone + 'static,
+ SendFn: Fn(String) + Clone + 'static,
+ SendBytesFn: Fn(Vec) + Clone + 'static,
+{
+ /// The current state of the `WebSocket` connection.
+ pub ready_state: ReadSignal,
+ /// Latest text message received from `WebSocket`.
+ pub message: ReadSignal