diff --git a/Cargo.toml b/Cargo.toml
index dfa6b92b0..3af02c491 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,6 +17,7 @@ members = [
"examples/counter-isomorphic/client",
"examples/counter-isomorphic/server",
"examples/counter-isomorphic/counter",
+ "examples/counter-isomorphic-sfa",
"examples/counters",
"examples/counters-stable",
"examples/fetch",
diff --git a/examples/counter-isomorphic-sfa/Cargo.toml b/examples/counter-isomorphic-sfa/Cargo.toml
new file mode 100644
index 000000000..429930900
--- /dev/null
+++ b/examples/counter-isomorphic-sfa/Cargo.toml
@@ -0,0 +1,35 @@
+[package]
+name = "leptos-counter-isomorphic"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+actix-files = { version = "0.6", optional = true }
+actix-web = { version = "4", optional = true, features = ["openssl", "macros"] }
+console_log = "0.2"
+console_error_panic_hook = "0.1"
+futures = "0.3"
+cfg-if = "1"
+leptos = { path = "../../../leptos/leptos", default-features = false, features = [
+ "serde",
+] }
+leptos_meta = { path = "../../../leptos/meta", default-features = false }
+leptos_router = { path = "../../../leptos/router", default-features = false }
+log = "0.4"
+simple_logger = "2"
+#counter = { path = "../counter", default-features = false}
+
+[features]
+default = ["csr"]
+csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
+hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
+ssr = [
+ "dep:actix-files",
+ "dep:actix-web",
+ "leptos/ssr",
+ "leptos_meta/ssr",
+ "leptos_router/ssr",
+]
diff --git a/examples/counter-isomorphic-sfa/LICENSE b/examples/counter-isomorphic-sfa/LICENSE
new file mode 100644
index 000000000..77d5625cb
--- /dev/null
+++ b/examples/counter-isomorphic-sfa/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Greg Johnston
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/examples/counter-isomorphic-sfa/README.md b/examples/counter-isomorphic-sfa/README.md
new file mode 100644
index 000000000..27e7a7d92
--- /dev/null
+++ b/examples/counter-isomorphic-sfa/README.md
@@ -0,0 +1,18 @@
+# Leptos Counter Isomorphic Example
+
+This example demonstrates how to use a function isomorphically, to run a server side function from the browser and receive a result.
+
+## Server Side Rendering With Hydration
+To run it as a server side app with hydration, first you should run
+```bash
+wasm-pack build --target=web --no-default-features --features=hydrate
+```
+to generate the Webassembly to provide hydration features for the server.
+Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
+```bash
+cargo run --no-default-features --features=ssr`
+```
+> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
+> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!
+
+If for some reason you want to run it as a fully client side app, that can be done with the instructions below.
diff --git a/examples/counter-isomorphic-sfa/index.html b/examples/counter-isomorphic-sfa/index.html
new file mode 100644
index 000000000..b5ec4d243
--- /dev/null
+++ b/examples/counter-isomorphic-sfa/index.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/counter-isomorphic-sfa/src/counters.rs b/examples/counter-isomorphic-sfa/src/counters.rs
new file mode 100644
index 000000000..20413e865
--- /dev/null
+++ b/examples/counter-isomorphic-sfa/src/counters.rs
@@ -0,0 +1,237 @@
+use leptos::*;
+use leptos_router::*;
+use serde::{Deserialize, Serialize};
+
+#[cfg(feature = "ssr")]
+use std::sync::atomic::{AtomicI32, Ordering};
+
+#[cfg(feature = "ssr")]
+use broadcaster::BroadcastChannel;
+
+#[cfg(feature = "ssr")]
+pub fn register_server_functions() {
+ GetServerCount::register();
+ AdjustServerCount::register();
+ ClearServerCount::register();
+}
+
+#[cfg(feature = "ssr")]
+static COUNT: AtomicI32 = AtomicI32::new(0);
+
+#[cfg(feature = "ssr")]
+lazy_static::lazy_static! {
+ pub static ref COUNT_CHANNEL: BroadcastChannel = BroadcastChannel::new();
+}
+// "/api" is an optional prefix that allows you to locate server functions wherever you'd like on the server
+#[server(GetServerCount, "/api")]
+pub async fn get_server_count() -> Result {
+ Ok(COUNT.load(Ordering::Relaxed))
+}
+
+#[server(AdjustServerCount, "/api")]
+pub async fn adjust_server_count(delta: i32, msg: String) -> Result {
+ let new = COUNT.load(Ordering::Relaxed) + delta;
+ COUNT.store(new, Ordering::Relaxed);
+ _ = COUNT_CHANNEL.send(&new).await;
+ println!("message = {:?}", msg);
+ Ok(new)
+}
+
+#[server(ClearServerCount, "/api")]
+pub async fn clear_server_count() -> Result {
+ COUNT.store(0, Ordering::Relaxed);
+ _ = COUNT_CHANNEL.send(&0).await;
+ Ok(0)
+}
+#[component]
+pub fn Counters(cx: Scope) -> Element {
+ view! {
+ cx,
+
+
+
+
"Server-Side Counters"
+
"Each of these counters stores its data in the same variable on the server."
+
"The value is shared across connections. Try opening this is another browser tab to see what I mean."
+
+
+
+
+
+ }/>
+
+ }/>
+
+ }/>
+
+
+
+
+ }
+}
+
+// This is an example of "single-user" server functions
+// The counter value is loaded from the server, and re-fetches whenever
+// it's invalidated by one of the user's own actions
+// This is the typical pattern for a CRUD app
+#[component]
+pub fn Counter(cx: Scope) -> Element {
+ let dec = create_action(cx, |_| adjust_server_count(-1, "decing".into()));
+ let inc = create_action(cx, |_| adjust_server_count(1, "incing".into()));
+ let clear = create_action(cx, |_| clear_server_count());
+ let counter = create_resource(
+ cx,
+ move || (dec.version.get(), inc.version.get(), clear.version.get()),
+ |_| get_server_count(),
+ );
+
+ let value = move || counter.read().map(|count| count.unwrap_or(0)).unwrap_or(0);
+ let error_msg = move || {
+ counter
+ .read()
+ .map(|res| match res {
+ Ok(_) => None,
+ Err(e) => Some(e),
+ })
+ .flatten()
+ };
+
+ view! {
+ cx,
+
+
"Simple Counter"
+
"This counter sets the value on the server and automatically reloads the new value."
+ }
+}
+
+// This is the counter
+// It uses the same invalidation pattern as the plain counter,
+// but uses HTML forms to submit the actions
+#[component]
+pub fn FormCounter(cx: Scope) -> Element {
+ let adjust = create_server_action::(cx);
+ let clear = create_server_action::(cx);
+
+ let counter = create_resource(
+ cx,
+ {
+ let adjust = adjust.version;
+ let clear = clear.version;
+ move || (adjust.get(), clear.get())
+ },
+ |_| {
+ log::debug!("FormCounter running fetcher");
+
+ get_server_count()
+ },
+ );
+ let value = move || {
+ log::debug!("FormCounter looking for value");
+ counter
+ .read()
+ .map(|n| n.ok())
+ .flatten()
+ .map(|n| n)
+ .unwrap_or(0)
+ };
+
+ let adjust2 = adjust.clone();
+
+ view! {
+ cx,
+
+
"Form Counter"
+
"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"
+
+ // calling a server function is the same as POSTing to its API URL
+ // so we can just do that with a form and button
+
+
+
+ // We can submit named arguments to the server functions
+ // by including them as input values with the same name
+
+
+
+
+
+ "Value: " {move || value().to_string()} "!"
+
+
+
+
+
+
+
+ }
+}
+
+// This is a kind of "multi-user" counter
+// It relies on a stream of server-sent events (SSE) for the counter's value
+// Whenever another user updates the value, it will update here
+// This is the primitive pattern for live chat, collaborative editing, etc.
+#[component]
+pub fn MultiuserCounter(cx: Scope) -> Element {
+ let dec = create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
+ let inc = create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
+ let clear = create_action(cx, |_| clear_server_count());
+
+ #[cfg(not(feature = "ssr"))]
+ let multiplayer_value = {
+ use futures::StreamExt;
+
+ let mut source = gloo::net::eventsource::futures::EventSource::new("/api/events")
+ .expect_throw("couldn't connect to SSE stream");
+ let s = create_signal_from_stream(
+ cx,
+ source.subscribe("message").unwrap().map(|value| {
+ value
+ .expect_throw("no message event")
+ .1
+ .data()
+ .as_string()
+ .expect_throw("expected string value")
+ }),
+ );
+
+ on_cleanup(cx, move || source.close());
+ s
+ };
+
+ #[cfg(feature = "ssr")]
+ let multiplayer_value =
+ create_signal_from_stream(cx, futures::stream::once(Box::pin(async { 0.to_string() })));
+
+ view! {
+ cx,
+
+
"Multi-User Counter"
+
"This one uses server-sent events (SSE) to live-update when other users make changes."