diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -200,7 +200,7 @@
 app = vcomp 0 updateModel viewModel
 ----------------------------------------------------------------------------
 -- | Updates model, optionally introduces side effects
-updateModel :: Action -> Effect parent Int Action
+updateModel :: Action -> Effect parent props Int Action
 updateModel = \case
   AddOne -> this += 1
   SubtractOne -> this -= 1
@@ -209,8 +209,8 @@
     consoleLog "Hello World"
 ----------------------------------------------------------------------------
 -- | Constructs a virtual DOM from a model
-viewModel :: Int -> View Int Action
-viewModel x = vfrag
+viewModel :: () -> Int -> View Int Action
+viewModel _props x = vfrag
     [ H.button_ [ H.onClick AddOne ] [ text "+" ]
     , text (ms x)
     , H.button_ [ H.onClick SubtractOne ] [ text "-" ]
diff --git a/js/miso.js b/js/miso.js
--- a/js/miso.js
+++ b/js/miso.js
@@ -282,6 +282,8 @@
       n.componentId = c.componentId;
       if (c.child)
         c.child.parent = n;
+      if (n.diffProps)
+        n.diffProps();
       return;
     }
     replace(c, n, parent, context);
diff --git a/js/miso.prod.js b/js/miso.prod.js
--- a/js/miso.prod.js
+++ b/js/miso.prod.js
@@ -1,1 +1,1 @@
-var m="1.9.0.0";function y(U,z){var G=function(){var H=document.getElementById(U);if(H&&H.focus)H.focus()};z>0?setTimeout(G,z):G()}function h(U,z){var G=function(){var H=document.getElementById(U);if(H&&H.blur)H.blur()};z>0?setTimeout(G,z):G()}function u(U,z){var G=function(){var H=document.getElementById(U);if(H&&typeof H.select==="function")H.select()};z>0?setTimeout(G,z):G()}function g(U,z,G,H){var K=function(){var Q=document.getElementById(U);if(Q&&typeof Q.setSelectionRange==="function")Q.setSelectionRange(z,G,"none")};H>0?setTimeout(K,H):K()}function v(U,z,G,H,K,Q,X){var Y={method:z,headers:H};if(G)Y.body=G;let $={},_=null;try{fetch(U,Y).then((Z)=>{_=Z.status;for(let[B,S]of Z.headers)$[B]=S;if(!Z.ok)throw Error(Z.statusText);if(X=="json")return Z.json();else if(X=="text")return Z.text();else if(X==="arrayBuffer")return Z.arrayBuffer();else if(X==="blob")return Z.blob();else if(X==="bytes")return Z.bytes();else if(X==="formData")return Z.formData();else if(X==="none")return K({error:null,body:null,headers:$,status:_})}).then((Z)=>K({error:null,body:Z,headers:$,status:_})).catch((Z)=>Q({error:null,body:Z,headers:$,status:_}))}catch(Z){Q({body:null,error:Z.message,headers:$,status:_})}}function l(U,z,G,H,K,Q,X,Y,$){try{let _=new WebSocket(U);return _.onopen=function(){z()},_.onclose=function(Z){G(Z)},_.onerror=function(Z){console.error(Z),Y("WebSocket error received")},_.onmessage=function(Z){if(typeof Z.data==="string")try{if($){if(H)H(Z.data);return}let B=JSON.parse(Z.data);if(K)K(B)}catch(B){if($&&H)H(Z.data);else Y(B.message)}else if(Z.data instanceof Blob){if(Q)Q(Z.data)}else if(Z.data instanceof ArrayBuffer){if(X)X(Z.data)}else console.error("Received unknown message type from WebSocket",Z),Y("Unknown message received from WebSocket")},_}catch(_){Y(_.message)}}function x(U){if(U)U.close(),U=null}function i(U,z){if(z&&U&&U.readyState===WebSocket.OPEN)U.send(z)}function d(U,z,G,H,K,Q){try{let X=new EventSource(U);return X.onopen=function(){z()},X.onerror=function(){K("EventSource error received")},X.onmessage=function(Y){try{if(Q){if(G)G(Y.data);return}let $=JSON.parse(Y.data);if(H)H($)}catch($){if(Q&&G)G(Y.data);else K($.message)}},X}catch(X){K(X.message)}}function p(U){if(U)U.close(),U=null}function a(U,z){if(!U.classList)U.classList=new Set;for(let G of z)for(let H of G.trim().split(" "))if(H)U.classList.add(H)}function s(U,z){if(!U.parent)return;z.nextSibling=U.nextSibling,z.parent=U.parent,U.parent.child=z}function r(U,z={}){let G=Object.keys(z),H=Object.values(z);return Function(...G,U)(...H)}function c(U){if(U===null||U===void 0)return 0;if(typeof U==="number")return 1;if(typeof U==="string")return 2;if(typeof U==="boolean")return 3;if(Array.isArray(U))return 4;return 5}function o(U){return function(){U|=0,U=U+2654435769|0;var z=U^U>>>15;return z=Math.imul(z,2246822507),z=z^z>>>13,z=Math.imul(z,3266489909),((z^z>>>16)>>>0)/4294967296}}function n(){let U=new Uint32Array(1);return crypto.getRandomValues(U)[0]}function t(){return Math.random()}function W(U,z){switch(U.type){case 3:for(let G of U.children)W(G,z);break;case 0:if(U.child)W(U.child,z);break;default:z(U.domRef);break}}function q(U){switch(U.type){case 3:{if(!U.children||U.children.length===0)return null;return q(U.children[0])}case 0:if(!U.child)return null;return q(U.child);default:return U.domRef}}function N(U){switch(U.type){case 3:{if(!U.children||U.children.length===0)return null;return N(U.children[U.children.length-1])}case 0:if(!U.child)return null;return N(U.child);default:return U.domRef}}function A(U,z,G,H){if(!U&&!z)return;else if(!U)D(z,G,H);else if(!z)O(U,G,H);else if(U.type===2&&z.type===2)JU(U,z,H);else if(U.type===0&&z.type===0){if(z.key===U.key){if(z.child=U.child,z.componentId=U.componentId,U.child)U.child.parent=z;return}V(U,z,G,H)}else if(U.type===3&&z.type===3)if(z.key===U.key){let K=N(U),Q=K?K.nextSibling:H.nextSibling(U);XU(U.children,z.children,G,H,Q)}else V(U,z,G,H);else if(U.type===1&&z.type===1)if(z.tag===U.tag&&z.key===U.key)z.domRef=U.domRef,QU(U,z,H);else V(U,z,G,H);else V(U,z,G,H)}function JU(U,z,G){if(U.text!==z.text)G.setTextContent(U.domRef,z.text);z.domRef=U.domRef;return}function V(U,z,G,H){if(U.type===3){let X=N(U),Y=X?X.nextSibling:H.nextSibling(U);if(O(U,G,H),Y)C(G,2,Y,z,H);else D(z,G,H);return}switch(U.type){case 2:break;default:P(U);break}let K=q(U),Q=N(U);if(!K||!Q){let X=H.nextSibling(U);if(X)C(G,2,X,z,H);else D(z,G,H)}else if(K!==Q){let X=Q.nextSibling;if(W(U,(Y)=>H.removeChild(G,Y)),X)C(G,2,X,z,H);else D(z,G,H)}else C(G,1,K,z,H);switch(U.type){case 2:break;default:R(U);break}}function O(U,z,G){switch(U.type){case 2:break;case 3:for(let H of U.children)O(H,z,G);return;default:P(U);break}switch(W(U,(H)=>G.removeChild(z,H)),U.type){case 2:break;default:R(U);break}}function R(U){if(U.type===3){for(let z of U.children)if(z.type!==2)R(z);return}switch(AU(U),U.type){case 1:for(let z of U.children)if(z.type!==2)R(z);break;case 0:if(U.child&&U.child.type!==2)R(U.child);break}}function AU(U){if(U.type===1&&U.onDestroyed)U.onDestroyed();if(U.type===0)wU(U)}function BU(U){switch(U.type){case 0:break;case 1:if(U.onBeforeDestroyed)U.onBeforeDestroyed();break;default:break}}function P(U){if(U.type===3){for(let z of U.children)if(z.type!==2)P(z);return}switch(BU(U),U.type){case 1:for(let z of U.children){if(z.type===2)continue;P(z)}break;case 0:if(U.child&&U.child.type!==2)P(U.child);break}}function QU(U,z,G){NU(U?U.props:{},z.props,z.domRef,z.ns==="svg",G),SU(U?U.classList:null,z.classList,z.domRef,G),CU(U?U.css:{},z.css,z.domRef,G),XU(U?U.children:[],z.children,z.domRef,G),IU(z)}function SU(U,z,G,H){if(!U&&!z)return;if(!U){for(let K of z)H.addClass(K,G);return}if(!z){for(let K of U)H.removeClass(K,G);return}for(let K of U)if(!z.has(K))H.removeClass(K,G);for(let K of z)if(!U.has(K))H.addClass(K,G);return}function NU(U,z,G,H,K){var Q;for(let X in U)if(Q=z[X],Q===void 0)if(H||!(X in G)||X==="disabled")K.removeAttribute(G,X);else K.setAttribute(G,X,"");else{if(Q===U[X]&&X!=="checked"&&X!=="value")continue;if(H)if(X==="href")K.setAttributeNS(G,"http://www.w3.org/1999/xlink","href",Q);else K.setAttribute(G,X,Q);else if(X in G&&!(X==="list"||X==="form"))G[X]=Q;else K.setAttribute(G,X,Q)}for(let X in z){if(U&&X in U)continue;if(Q=z[X],H)if(X==="href")K.setAttributeNS(G,"http://www.w3.org/1999/xlink","href",Q);else K.setAttribute(G,X,Q);else if(X in G&&!(X==="list"||X==="form"))G[X]=z[X];else K.setAttribute(G,X,Q)}}function CU(U,z,G,H){H.setInlineStyle(U,z,G)}function EU(U,z){if(U.length===0||z.length===0)return!1;for(var G=0;G<U.length;G++)if(U[G].key===null||U[G].key===void 0)return!1;for(var G=0;G<z.length;G++)if(z[G].key===null||z[G].key===void 0)return!1;return!0}function XU(U,z,G,H,K=null){if(EU(U,z))PU(U,z,G,H,K);else for(let Q=0;Q<Math.max(z.length,U.length);Q++){let X=U[Q],Y=z[Q];if(!X&&Y)if(K)C(G,2,K,Y,H);else D(Y,G,H);else A(X,Y,G,H)}}function DU(U,z){if(U.ns==="svg")U.domRef=z.createElementNS("http://www.w3.org/2000/svg",U.tag);else if(U.ns==="mathml")U.domRef=z.createElementNS("http://www.w3.org/1998/Math/MathML",U.tag);else U.domRef=z.createElement(U.tag)}function YU(U,z,G){if(z.onCreated)z.onCreated(z.domRef)}function C(U,z,G,H,K){switch(H.type){case 2:switch(H.domRef=K.createTextNode(H.text),z){case 2:K.insertBefore(U,H.domRef,G);break;case 0:K.appendChild(U,H.domRef);break;case 1:K.replaceChild(U,H.domRef,G);break}break;case 3:for(let Q of H.children)C(U,2,G,Q,K);if(z===1&&G)K.removeChild(U,G);break;case 0:MU(U,z,G,H,K);break;case 1:if(H.onBeforeCreated)H.onBeforeCreated();if(DU(H,K),H.onCreated)H.onCreated(H.domRef);switch(QU(null,H,K),z){case 2:K.insertBefore(U,H.domRef,G);break;case 0:K.appendChild(U,H.domRef);break;case 1:K.replaceChild(U,H.domRef,G);break}break}}function IU(U){if(U.tag==="canvas"&&U.draw)U.draw(U.domRef)}function wU(U){U.unmount(U.componentId)}function MU(U,z,G,H,K){let Q=H.mount(U);H.componentId=Q.componentId,H.child=Q.componentTree,Q.componentTree.parent=H;let X=q(Q.componentTree);if(Q.componentTree.type!==0){if(z===1&&G)if(!X)K.removeChild(U,G);else if(Q.componentTree.type===3)W(Q.componentTree,(Y)=>K.insertBefore(U,Y,G)),K.removeChild(U,G);else K.replaceChild(U,X,G);else if(z===2)if(G)W(Q.componentTree,(Y)=>K.insertBefore(U,Y,G));else W(Q.componentTree,(Y)=>K.appendChild(U,Y))}}function D(U,z,G){C(z,0,null,U,G)}function KU(U,z,G,H){let K=G?q(G)??H.nextSibling(G):null;if(K)W(z,(Q)=>H.insertBefore(U,Q,K));else W(z,(Q)=>H.appendChild(U,Q))}function RU(U,z,G,H){let K=q(U),Q=q(z);if(K&&Q&&(U.type===1||U.type===2)&&(z.type===1||z.type===2)){H.swapDOMRefs(K,Q,G);return}let X=N(U),Y=X?X.nextSibling:H.nextSibling(U),$=q(z)??H.nextSibling(z);if($)W(U,(_)=>H.insertBefore(G,_,$));else W(U,(_)=>H.appendChild(G,_));if(Y)W(z,(_)=>H.insertBefore(G,_,Y));else W(z,(_)=>H.appendChild(G,_))}function PU(U,z,G,H,K=null){var Q=0,X=0,Y=U.length-1,$=z.length-1,_,Z,B,S,J,f,HU;for(;;){if(X>$&&Q>Y)break;if(Z=z[X],B=z[$],J=U[Q],S=U[Y],Q>Y){let M=(J?q(J)??H.nextSibling(J):null)??K;if(M)C(G,2,M,Z,H);else D(Z,G,H);U.splice(X,0,Z),X++}else if(X>$){_=Y;while(Y>=Q)O(U[Y--],G,H);U.splice(Q,_-Q+1);break}else if(J.key===Z.key)A(U[Q++],z[X++],G,H);else if(S.key===B.key)A(U[Y--],z[$--],G,H);else if(J.key===B.key&&Z.key===S.key)RU(S,J,G,H),FU(U,Q,Y),A(U[Q++],z[X++],G,H),A(U[Y--],z[$--],G,H);else if(J.key===B.key){let I=N(S),M=I?I.nextSibling:H.nextSibling(S);if(M)W(J,(T)=>H.insertBefore(G,T,M));else W(J,(T)=>H.appendChild(G,T));U.splice(Y,0,U.splice(Q,1)[0]),A(U[Y--],z[$--],G,H)}else if(S.key===Z.key)KU(G,S,J,H),U.splice(Q,0,U.splice(Y,1)[0]),A(U[Q++],Z,G,H),X++;else{f=!1,_=Q;while(_<=Y){if(U[_].key===Z.key){f=!0,HU=U[_];break}_++}if(f)U.splice(Q,0,U.splice(_,1)[0]),A(U[Q++],Z,G,H),KU(G,HU,U[Q],H),X++;else{let I=q(J)??H.nextSibling(J);if(I)C(G,2,I,Z,H);else D(Z,G,H);U.splice(Q++,0,Z),X++,Y++}}}}function FU(U,z,G){let H=U[z];U[z]=U[G],U[G]=H}function e(U,z,G,H,K){for(let Q of z)K.addEventListener(U,Q.name,function(X){LU(X,U,G,H,K)},Q.capture)}function LU(U,z,G,H,K){G(function(Q){if(Array.isArray(U))for(let X of U)ZU(X,Q,z,H,K);else ZU(U,Q,z,H,K)})}function ZU(U,z,G,H,K){var Q=K.getTarget(U);if(Q){let X=VU(G,Q,K);E(U,z,X,H,K)}}function VU(U,z,G){var H=[];while(!G.isEqual(U,z))if(H.unshift(z),z&&G.parentNode(z))z=G.parentNode(z);else return H;return H}function E(U,z,G,H,K){if(!G.length){if(H)console.warn('Event "'+U.type+'" did not find an event handler to dispatch on',z,U);return}else if(G.length>1){if(z.type===2)return;else if(z.type===3){for(let Q of z.children)if(F(Q,G[0],K)){E(U,Q,G,H,K);return}return}else if(z.type===0){if(!z.child){if(H)throw console.error("VComp has no child property set during event delegation",z),console.error("This means the Component has not been fully mounted, this should never happen"),Error("VComp has no .child property set during event delegation");return}return E(U,z.child,G,H,K)}else if(z.type===1){if(K.isEqual(z.domRef,G[0])){let Q=z.events.captures[U.type];if(Q){let X=Q.options;if(X.preventDefault)U.preventDefault();if(!U.captureStopped)Q.runEvent(U,z.domRef);if(X.stopPropagation)U.captureStopped=!0}G.splice(0,1);for(let X of z.children)if(F(X,G[0],K)){E(U,X,G,H,K);return}}return}}else if(z.type===0){if(z.child)E(U,z.child,G,H,K)}else if(z.type===3){for(let Q of z.children)if(F(Q,G[0],K)){E(U,Q,G,H,K);return}}else if(z.type===1){let Q=z.events.captures[U.type];if(Q&&!U.captureStopped){let Y=Q.options;if(K.isEqual(G[0],z.domRef)){if(Y.preventDefault)U.preventDefault();if(Q.runEvent(U,G[0]),Y.stopPropagation)U.captureStopped=!0}}let X=z.events.bubbles[U.type];if(X&&!U.captureStopped){let Y=X.options;if(K.isEqual(G[0],z.domRef)){if(Y.preventDefault)U.preventDefault();if(X.runEvent(U,G[0]),!Y.stopPropagation)$U(z.parent,U)}}else if(!U.captureStopped)$U(z.parent,U)}}function $U(U,z){while(U)switch(U.type){case 2:break;case 3:U=U.parent;break;case 1:let G=U.events.bubbles[z.type];if(G){let H=G.options;if(H.preventDefault)z.preventDefault();if(G.runEvent(z,U.domRef),H.stopPropagation)return}U=U.parent;break;case 0:if(!U.eventPropagation)return;U=U.parent;break}}function L(U,z){if(typeof U[0]==="object"){var G=[];for(var H=0;H<U.length;H++)G.push(L(U[H],z));return G}for(let Y of U)z=z[Y];var K;if(z instanceof Array||"length"in z&&z.localName!=="select"){K=[];for(var Q=0;Q<z.length;Q++)K.push(L([],z[Q]));return K}K={};for(var X in OU(z)){if(z.localName==="input"&&(X==="selectionDirection"||X==="selectionStart"||X==="selectionEnd"))continue;if(typeof z[X]=="string"||typeof z[X]=="number"||typeof z[X]=="boolean")K[X]=z[X]}return K}function F(U,z,G){switch(U.type){case 3:for(let H of U.children)if(F(H,z,G))return!0;return!1;case 0:return U.child?F(U.child,z,G):!1;default:return G.isEqual(U.domRef,z)}}function OU(U){var z={},G=0;do{var H=Object.getOwnPropertyNames(U);for(G=0;G<H.length;G++)z[H[G]]=null}while(U=Object.getPrototypeOf(U));return z}var j={addEventListener:(U,z,G,H)=>{U.addEventListener(z,G,H)},delegator:(U,z,G,H,K)=>{e(U,z,G,H,K)},isEqual:(U,z)=>{return U===z},getTarget:(U)=>{return U.target},parentNode:(U)=>{return U.parentNode}},_U={getInlineStyle:(U,z)=>{return U.style[z]},firstChild:(U)=>{return U.firstChild},lastChild:(U)=>{return U.lastChild},getAttribute:(U,z)=>{if(z==="class")return U.className;if(z in U)return U[z];return U.getAttribute(z)},getTag:(U)=>{return U.nodeName},getTextContent:(U)=>{return U.textContent},children:(U)=>{return U.childNodes}},WU={mountComponent:function(U,z){return},unmountComponent:function(U){return},modelHydration:function(U,z){return}},UU={nextSibling:(U)=>{let z=U.nextSibling;while(z)switch(z.type){case 0:case 3:{let G=q(z);if(G)return G;z=z.nextSibling;break}default:return z.domRef}return null},createTextNode:(U)=>{return document.createTextNode(U)},createElementNS:(U,z)=>{return document.createElementNS(U,z)},appendChild:(U,z)=>{return U.appendChild(z)},replaceChild:(U,z,G)=>{return U.replaceChild(z,G)},removeChild:(U,z)=>{return U.removeChild(z)},createElement:(U)=>{return document.createElement(U)},addClass:(U,z)=>{if(U)z.classList.add(U)},removeClass:(U,z)=>{if(U)z.classList.remove(U)},insertBefore:(U,z,G)=>{return U.insertBefore(z,G)},swapDOMRefs:(U,z,G)=>{let H=U.nextSibling;G.insertBefore(U,z),G.insertBefore(z,H);return},setInlineStyle:(U,z,G)=>{var H;for(let K in U)if(H=z[K],!H)if(K in G.style)G.style[K]="";else G.style.setProperty(K,"");else if(H!==U[K])if(K in G.style)G.style[K]=H;else G.style.setProperty(K,H);for(let K in z){if(U&&U[K])continue;if(K in G.style)G.style[K]=z[K];else G.style.setProperty(K,z[K])}return},setAttribute:(U,z,G)=>{return U.setAttribute(z,G)},setAttributeNS:(U,z,G,H)=>{return U.setAttributeNS(z,G,H)},removeAttribute:(U,z)=>{return U.removeAttribute(z)},setTextContent:(U,z)=>{U.textContent=z;return},flush:()=>{return},getHead:function(){return document.head},getRoot:function(){return document.body}};function zU(U){var z=0,G=U.length>0?[U[0]]:[];for(var H=1;H<U.length;H++){if(G[z].type===2&&U[H].type===2){G[z].text+=U[H].text;continue}G[++z]=U[H]}for(let K of G)if(K.type===3)K.children=zU(K.children);return G}function GU(U,z,G,H,K){if(!G||!z)return!1;if(z.nodeType===3)return!1;if(!b(U,G,H.firstChild(z),H,K)){if(U)console.warn("[DEBUG_HYDRATE] Could not copy DOM into virtual DOM, falling back to diff");while(H.firstChild(z))K.removeChild(z,H.lastChild(z));return!1}else if(U)console.info("[DEBUG_HYDRATE] Successfully prerendered page");return!0}function k(U,z,G){if(U)console.warn("[DEBUG_HYDRATE] VTree differed from node",z,G)}function qU(U,z){let G=N(U);return G?G.nextSibling:z}function b(U,z,G,H,K){switch(z.type){case 0:let X=z.mount(G.parentNode);if(z.componentId=X.componentId,z.child=X.componentTree,X.componentTree.parent=z,!b(U,z.child,G,H,K))return!1;break;case 3:z.children=zU(z.children);for(let $ of z.children){if(!G)return k(U,$,null),!1;if(!b(U,$,G,H,K))return!1;G=qU($,G)}break;case 2:if(G.nodeType!==3||z.text.trim()!==G.textContent.trim())return k(U,z,G),!1;z.domRef=G;break;case 1:if(G.nodeType!==1)return k(U,z,G),!1;z.domRef=G,z.children=zU(z.children),YU(G,z,K);let Y=G.firstChild;for(var Q=0;Q<z.children.length;Q++){let $=z.children[Q];if(!Y)return k(U,$,null),!1;if(!b(U,$,Y,H,K))return!1;Y=qU($,Y)}break}return!0}globalThis.miso={hydrationContext:_U,eventContext:j,drawingContext:UU,componentContext:WU,diff:A,hydrate:GU,version:m,callBlur:h,callFocus:y,callSelect:u,callSetSelectionRange:g,eventJSON:L,fetchCore:v,eventSourceConnect:d,eventSourceClose:p,websocketConnect:l,websocketClose:x,websocketSend:i,updateRef:s,inline:r,typeOf:c,mathRandom:t,getRandomValues:n,splitmix32:o,populateClass:a,delegateEvent:E,delegator:j.delegator,setDrawingContext:function(U){let z=globalThis[U].drawingContext,G=globalThis[U].eventContext,H=globalThis[U].componentContext;if(!z)console.error('Custom rendering engine ("drawingContext") is not defined at globalThis[name].drawingContext',U);if(!G)console.error('Custom event delegation ("eventContext") is not defined at globalThis[name].eventContext',U);if(!H)console.error('Custom component context ("componentContext") is not defined at globalThis[name].componentContext',U);globalThis.miso.drawingContext=z,globalThis.miso.eventContext=G,globalThis.miso.componentContext=H}};
+var m="1.9.0.0";function y(U,z){var G=function(){var H=document.getElementById(U);if(H&&H.focus)H.focus()};z>0?setTimeout(G,z):G()}function h(U,z){var G=function(){var H=document.getElementById(U);if(H&&H.blur)H.blur()};z>0?setTimeout(G,z):G()}function u(U,z){var G=function(){var H=document.getElementById(U);if(H&&typeof H.select==="function")H.select()};z>0?setTimeout(G,z):G()}function g(U,z,G,H){var K=function(){var Q=document.getElementById(U);if(Q&&typeof Q.setSelectionRange==="function")Q.setSelectionRange(z,G,"none")};H>0?setTimeout(K,H):K()}function v(U,z,G,H,K,Q,X){var Y={method:z,headers:H};if(G)Y.body=G;let $={},_=null;try{fetch(U,Y).then((Z)=>{_=Z.status;for(let[B,S]of Z.headers)$[B]=S;if(!Z.ok)throw new Error(Z.statusText);if(X=="json")return Z.json();else if(X=="text")return Z.text();else if(X==="arrayBuffer")return Z.arrayBuffer();else if(X==="blob")return Z.blob();else if(X==="bytes")return Z.bytes();else if(X==="formData")return Z.formData();else if(X==="none")return K({error:null,body:null,headers:$,status:_})}).then((Z)=>K({error:null,body:Z,headers:$,status:_})).catch((Z)=>Q({error:null,body:Z,headers:$,status:_}))}catch(Z){Q({body:null,error:Z.message,headers:$,status:_})}}function l(U,z,G,H,K,Q,X,Y,$){try{let _=new WebSocket(U);return _.onopen=function(){z()},_.onclose=function(Z){G(Z)},_.onerror=function(Z){console.error(Z),Y("WebSocket error received")},_.onmessage=function(Z){if(typeof Z.data==="string")try{if($){if(H)H(Z.data);return}let B=JSON.parse(Z.data);if(K)K(B)}catch(B){if($&&H)H(Z.data);else Y(B.message)}else if(Z.data instanceof Blob){if(Q)Q(Z.data)}else if(Z.data instanceof ArrayBuffer){if(X)X(Z.data)}else console.error("Received unknown message type from WebSocket",Z),Y("Unknown message received from WebSocket")},_}catch(_){Y(_.message)}}function x(U){if(U)U.close(),U=null}function i(U,z){if(z&&U&&U.readyState===WebSocket.OPEN)U.send(z)}function d(U,z,G,H,K,Q){try{let X=new EventSource(U);return X.onopen=function(){z()},X.onerror=function(){K("EventSource error received")},X.onmessage=function(Y){try{if(Q){if(G)G(Y.data);return}let $=JSON.parse(Y.data);if(H)H($)}catch($){if(Q&&G)G(Y.data);else K($.message)}},X}catch(X){K(X.message)}}function p(U){if(U)U.close(),U=null}function a(U,z){if(!U.classList)U.classList=new Set;for(let G of z)for(let H of G.trim().split(" "))if(H)U.classList.add(H)}function s(U,z){if(!U.parent)return;z.nextSibling=U.nextSibling,z.parent=U.parent,U.parent.child=z}function r(U,z={}){let G=Object.keys(z),H=Object.values(z);return new Function(...G,U)(...H)}function c(U){if(U===null||U===void 0)return 0;if(typeof U==="number")return 1;if(typeof U==="string")return 2;if(typeof U==="boolean")return 3;if(Array.isArray(U))return 4;return 5}function o(U){return function(){U|=0,U=U+2654435769|0;var z=U^U>>>15;return z=Math.imul(z,2246822507),z=z^z>>>13,z=Math.imul(z,3266489909),((z^z>>>16)>>>0)/4294967296}}function n(){let U=new Uint32Array(1);return crypto.getRandomValues(U)[0]}function t(){return Math.random()}function W(U,z){switch(U.type){case 3:for(let G of U.children)W(G,z);break;case 0:if(U.child)W(U.child,z);break;default:z(U.domRef);break}}function q(U){switch(U.type){case 3:{if(!U.children||U.children.length===0)return null;return q(U.children[0])}case 0:if(!U.child)return null;return q(U.child);default:return U.domRef}}function N(U){switch(U.type){case 3:{if(!U.children||U.children.length===0)return null;return N(U.children[U.children.length-1])}case 0:if(!U.child)return null;return N(U.child);default:return U.domRef}}function A(U,z,G,H){if(!U&&!z)return;else if(!U)D(z,G,H);else if(!z)O(U,G,H);else if(U.type===2&&z.type===2)JU(U,z,H);else if(U.type===0&&z.type===0){if(z.key===U.key){if(z.child=U.child,z.componentId=U.componentId,U.child)U.child.parent=z;if(z.diffProps)z.diffProps();return}V(U,z,G,H)}else if(U.type===3&&z.type===3)if(z.key===U.key){let K=N(U),Q=K?K.nextSibling:H.nextSibling(U);XU(U.children,z.children,G,H,Q)}else V(U,z,G,H);else if(U.type===1&&z.type===1)if(z.tag===U.tag&&z.key===U.key)z.domRef=U.domRef,QU(U,z,H);else V(U,z,G,H);else V(U,z,G,H)}function JU(U,z,G){if(U.text!==z.text)G.setTextContent(U.domRef,z.text);z.domRef=U.domRef;return}function V(U,z,G,H){if(U.type===3){let X=N(U),Y=X?X.nextSibling:H.nextSibling(U);if(O(U,G,H),Y)C(G,2,Y,z,H);else D(z,G,H);return}switch(U.type){case 2:break;default:P(U);break}let K=q(U),Q=N(U);if(!K||!Q){let X=H.nextSibling(U);if(X)C(G,2,X,z,H);else D(z,G,H)}else if(K!==Q){let X=Q.nextSibling;if(W(U,(Y)=>H.removeChild(G,Y)),X)C(G,2,X,z,H);else D(z,G,H)}else C(G,1,K,z,H);switch(U.type){case 2:break;default:R(U);break}}function O(U,z,G){switch(U.type){case 2:break;case 3:for(let H of U.children)O(H,z,G);return;default:P(U);break}switch(W(U,(H)=>G.removeChild(z,H)),U.type){case 2:break;default:R(U);break}}function R(U){if(U.type===3){for(let z of U.children)if(z.type!==2)R(z);return}switch(AU(U),U.type){case 1:for(let z of U.children)if(z.type!==2)R(z);break;case 0:if(U.child&&U.child.type!==2)R(U.child);break}}function AU(U){if(U.type===1&&U.onDestroyed)U.onDestroyed();if(U.type===0)wU(U)}function BU(U){switch(U.type){case 0:break;case 1:if(U.onBeforeDestroyed)U.onBeforeDestroyed();break;default:break}}function P(U){if(U.type===3){for(let z of U.children)if(z.type!==2)P(z);return}switch(BU(U),U.type){case 1:for(let z of U.children){if(z.type===2)continue;P(z)}break;case 0:if(U.child&&U.child.type!==2)P(U.child);break}}function QU(U,z,G){NU(U?U.props:{},z.props,z.domRef,z.ns==="svg",G),SU(U?U.classList:null,z.classList,z.domRef,G),CU(U?U.css:{},z.css,z.domRef,G),XU(U?U.children:[],z.children,z.domRef,G),IU(z)}function SU(U,z,G,H){if(!U&&!z)return;if(!U){for(let K of z)H.addClass(K,G);return}if(!z){for(let K of U)H.removeClass(K,G);return}for(let K of U)if(!z.has(K))H.removeClass(K,G);for(let K of z)if(!U.has(K))H.addClass(K,G);return}function NU(U,z,G,H,K){var Q;for(let X in U)if(Q=z[X],Q===void 0)if(H||!(X in G)||X==="disabled")K.removeAttribute(G,X);else K.setAttribute(G,X,"");else{if(Q===U[X]&&X!=="checked"&&X!=="value")continue;if(H)if(X==="href")K.setAttributeNS(G,"http://www.w3.org/1999/xlink","href",Q);else K.setAttribute(G,X,Q);else if(X in G&&!(X==="list"||X==="form"))G[X]=Q;else K.setAttribute(G,X,Q)}for(let X in z){if(U&&X in U)continue;if(Q=z[X],H)if(X==="href")K.setAttributeNS(G,"http://www.w3.org/1999/xlink","href",Q);else K.setAttribute(G,X,Q);else if(X in G&&!(X==="list"||X==="form"))G[X]=z[X];else K.setAttribute(G,X,Q)}}function CU(U,z,G,H){H.setInlineStyle(U,z,G)}function EU(U,z){if(U.length===0||z.length===0)return!1;for(var G=0;G<U.length;G++)if(U[G].key===null||U[G].key===void 0)return!1;for(var G=0;G<z.length;G++)if(z[G].key===null||z[G].key===void 0)return!1;return!0}function XU(U,z,G,H,K=null){if(EU(U,z))PU(U,z,G,H,K);else for(let Q=0;Q<Math.max(z.length,U.length);Q++){let X=U[Q],Y=z[Q];if(!X&&Y)if(K)C(G,2,K,Y,H);else D(Y,G,H);else A(X,Y,G,H)}}function DU(U,z){if(U.ns==="svg")U.domRef=z.createElementNS("http://www.w3.org/2000/svg",U.tag);else if(U.ns==="mathml")U.domRef=z.createElementNS("http://www.w3.org/1998/Math/MathML",U.tag);else U.domRef=z.createElement(U.tag)}function YU(U,z,G){if(z.onCreated)z.onCreated(z.domRef)}function C(U,z,G,H,K){switch(H.type){case 2:switch(H.domRef=K.createTextNode(H.text),z){case 2:K.insertBefore(U,H.domRef,G);break;case 0:K.appendChild(U,H.domRef);break;case 1:K.replaceChild(U,H.domRef,G);break}break;case 3:for(let Q of H.children)C(U,2,G,Q,K);if(z===1&&G)K.removeChild(U,G);break;case 0:MU(U,z,G,H,K);break;case 1:if(H.onBeforeCreated)H.onBeforeCreated();if(DU(H,K),H.onCreated)H.onCreated(H.domRef);switch(QU(null,H,K),z){case 2:K.insertBefore(U,H.domRef,G);break;case 0:K.appendChild(U,H.domRef);break;case 1:K.replaceChild(U,H.domRef,G);break}break}}function IU(U){if(U.tag==="canvas"&&U.draw)U.draw(U.domRef)}function wU(U){U.unmount(U.componentId)}function MU(U,z,G,H,K){let Q=H.mount(U);H.componentId=Q.componentId,H.child=Q.componentTree,Q.componentTree.parent=H;let X=q(Q.componentTree);if(Q.componentTree.type!==0){if(z===1&&G)if(!X)K.removeChild(U,G);else if(Q.componentTree.type===3)W(Q.componentTree,(Y)=>K.insertBefore(U,Y,G)),K.removeChild(U,G);else K.replaceChild(U,X,G);else if(z===2)if(G)W(Q.componentTree,(Y)=>K.insertBefore(U,Y,G));else W(Q.componentTree,(Y)=>K.appendChild(U,Y))}}function D(U,z,G){C(z,0,null,U,G)}function KU(U,z,G,H){let K=G?q(G)??H.nextSibling(G):null;if(K)W(z,(Q)=>H.insertBefore(U,Q,K));else W(z,(Q)=>H.appendChild(U,Q))}function RU(U,z,G,H){let K=q(U),Q=q(z);if(K&&Q&&(U.type===1||U.type===2)&&(z.type===1||z.type===2)){H.swapDOMRefs(K,Q,G);return}let X=N(U),Y=X?X.nextSibling:H.nextSibling(U),$=q(z)??H.nextSibling(z);if($)W(U,(_)=>H.insertBefore(G,_,$));else W(U,(_)=>H.appendChild(G,_));if(Y)W(z,(_)=>H.insertBefore(G,_,Y));else W(z,(_)=>H.appendChild(G,_))}function PU(U,z,G,H,K=null){var Q=0,X=0,Y=U.length-1,$=z.length-1,_,Z,B,S,J,f,HU;for(;;){if(X>$&&Q>Y)break;if(Z=z[X],B=z[$],J=U[Q],S=U[Y],Q>Y){let M=(J?q(J)??H.nextSibling(J):null)??K;if(M)C(G,2,M,Z,H);else D(Z,G,H);U.splice(X,0,Z),X++}else if(X>$){_=Y;while(Y>=Q)O(U[Y--],G,H);U.splice(Q,_-Q+1);break}else if(J.key===Z.key)A(U[Q++],z[X++],G,H);else if(S.key===B.key)A(U[Y--],z[$--],G,H);else if(J.key===B.key&&Z.key===S.key)RU(S,J,G,H),FU(U,Q,Y),A(U[Q++],z[X++],G,H),A(U[Y--],z[$--],G,H);else if(J.key===B.key){let I=N(S),M=I?I.nextSibling:H.nextSibling(S);if(M)W(J,(T)=>H.insertBefore(G,T,M));else W(J,(T)=>H.appendChild(G,T));U.splice(Y,0,U.splice(Q,1)[0]),A(U[Y--],z[$--],G,H)}else if(S.key===Z.key)KU(G,S,J,H),U.splice(Q,0,U.splice(Y,1)[0]),A(U[Q++],Z,G,H),X++;else{f=!1,_=Q;while(_<=Y){if(U[_].key===Z.key){f=!0,HU=U[_];break}_++}if(f)U.splice(Q,0,U.splice(_,1)[0]),A(U[Q++],Z,G,H),KU(G,HU,U[Q],H),X++;else{let I=q(J)??H.nextSibling(J);if(I)C(G,2,I,Z,H);else D(Z,G,H);U.splice(Q++,0,Z),X++,Y++}}}}function FU(U,z,G){let H=U[z];U[z]=U[G],U[G]=H}function e(U,z,G,H,K){for(let Q of z)K.addEventListener(U,Q.name,function(X){LU(X,U,G,H,K)},Q.capture)}function LU(U,z,G,H,K){G(function(Q){if(Array.isArray(U))for(let X of U)ZU(X,Q,z,H,K);else ZU(U,Q,z,H,K)})}function ZU(U,z,G,H,K){var Q=K.getTarget(U);if(Q){let X=VU(G,Q,K);E(U,z,X,H,K)}}function VU(U,z,G){var H=[];while(!G.isEqual(U,z))if(H.unshift(z),z&&G.parentNode(z))z=G.parentNode(z);else return H;return H}function E(U,z,G,H,K){if(!G.length){if(H)console.warn('Event "'+U.type+'" did not find an event handler to dispatch on',z,U);return}else if(G.length>1){if(z.type===2)return;else if(z.type===3){for(let Q of z.children)if(F(Q,G[0],K)){E(U,Q,G,H,K);return}return}else if(z.type===0){if(!z.child){if(H)throw console.error("VComp has no child property set during event delegation",z),console.error("This means the Component has not been fully mounted, this should never happen"),new Error("VComp has no .child property set during event delegation");return}return E(U,z.child,G,H,K)}else if(z.type===1){if(K.isEqual(z.domRef,G[0])){let Q=z.events.captures[U.type];if(Q){let X=Q.options;if(X.preventDefault)U.preventDefault();if(!U.captureStopped)Q.runEvent(U,z.domRef);if(X.stopPropagation)U.captureStopped=!0}G.splice(0,1);for(let X of z.children)if(F(X,G[0],K)){E(U,X,G,H,K);return}}return}}else if(z.type===0){if(z.child)E(U,z.child,G,H,K)}else if(z.type===3){for(let Q of z.children)if(F(Q,G[0],K)){E(U,Q,G,H,K);return}}else if(z.type===1){let Q=z.events.captures[U.type];if(Q&&!U.captureStopped){let Y=Q.options;if(K.isEqual(G[0],z.domRef)){if(Y.preventDefault)U.preventDefault();if(Q.runEvent(U,G[0]),Y.stopPropagation)U.captureStopped=!0}}let X=z.events.bubbles[U.type];if(X&&!U.captureStopped){let Y=X.options;if(K.isEqual(G[0],z.domRef)){if(Y.preventDefault)U.preventDefault();if(X.runEvent(U,G[0]),!Y.stopPropagation)$U(z.parent,U)}}else if(!U.captureStopped)$U(z.parent,U)}}function $U(U,z){while(U)switch(U.type){case 2:break;case 3:U=U.parent;break;case 1:let G=U.events.bubbles[z.type];if(G){let H=G.options;if(H.preventDefault)z.preventDefault();if(G.runEvent(z,U.domRef),H.stopPropagation)return}U=U.parent;break;case 0:if(!U.eventPropagation)return;U=U.parent;break}}function L(U,z){if(typeof U[0]==="object"){var G=[];for(var H=0;H<U.length;H++)G.push(L(U[H],z));return G}for(let Y of U)z=z[Y];var K;if(z instanceof Array||"length"in z&&z.localName!=="select"){K=[];for(var Q=0;Q<z.length;Q++)K.push(L([],z[Q]));return K}K={};for(var X in OU(z)){if(z.localName==="input"&&(X==="selectionDirection"||X==="selectionStart"||X==="selectionEnd"))continue;if(typeof z[X]=="string"||typeof z[X]=="number"||typeof z[X]=="boolean")K[X]=z[X]}return K}function F(U,z,G){switch(U.type){case 3:for(let H of U.children)if(F(H,z,G))return!0;return!1;case 0:return U.child?F(U.child,z,G):!1;default:return G.isEqual(U.domRef,z)}}function OU(U){var z={},G=0;do{var H=Object.getOwnPropertyNames(U);for(G=0;G<H.length;G++)z[H[G]]=null}while(U=Object.getPrototypeOf(U));return z}var j={addEventListener:(U,z,G,H)=>{U.addEventListener(z,G,H)},delegator:(U,z,G,H,K)=>{e(U,z,G,H,K)},isEqual:(U,z)=>{return U===z},getTarget:(U)=>{return U.target},parentNode:(U)=>{return U.parentNode}},_U={getInlineStyle:(U,z)=>{return U.style[z]},firstChild:(U)=>{return U.firstChild},lastChild:(U)=>{return U.lastChild},getAttribute:(U,z)=>{if(z==="class")return U.className;if(z in U)return U[z];return U.getAttribute(z)},getTag:(U)=>{return U.nodeName},getTextContent:(U)=>{return U.textContent},children:(U)=>{return U.childNodes}},WU={mountComponent:function(U,z){return},unmountComponent:function(U){return},modelHydration:function(U,z){return}},UU={nextSibling:(U)=>{let z=U.nextSibling;while(z)switch(z.type){case 0:case 3:{let G=q(z);if(G)return G;z=z.nextSibling;break}default:return z.domRef}return null},createTextNode:(U)=>{return document.createTextNode(U)},createElementNS:(U,z)=>{return document.createElementNS(U,z)},appendChild:(U,z)=>{return U.appendChild(z)},replaceChild:(U,z,G)=>{return U.replaceChild(z,G)},removeChild:(U,z)=>{return U.removeChild(z)},createElement:(U)=>{return document.createElement(U)},addClass:(U,z)=>{if(U)z.classList.add(U)},removeClass:(U,z)=>{if(U)z.classList.remove(U)},insertBefore:(U,z,G)=>{return U.insertBefore(z,G)},swapDOMRefs:(U,z,G)=>{let H=U.nextSibling;G.insertBefore(U,z),G.insertBefore(z,H);return},setInlineStyle:(U,z,G)=>{var H;for(let K in U)if(H=z[K],!H)if(K in G.style)G.style[K]="";else G.style.setProperty(K,"");else if(H!==U[K])if(K in G.style)G.style[K]=H;else G.style.setProperty(K,H);for(let K in z){if(U&&U[K])continue;if(K in G.style)G.style[K]=z[K];else G.style.setProperty(K,z[K])}return},setAttribute:(U,z,G)=>{return U.setAttribute(z,G)},setAttributeNS:(U,z,G,H)=>{return U.setAttributeNS(z,G,H)},removeAttribute:(U,z)=>{return U.removeAttribute(z)},setTextContent:(U,z)=>{U.textContent=z;return},flush:()=>{return},getHead:function(){return document.head},getRoot:function(){return document.body}};function zU(U){var z=0,G=U.length>0?[U[0]]:[];for(var H=1;H<U.length;H++){if(G[z].type===2&&U[H].type===2){G[z].text+=U[H].text;continue}G[++z]=U[H]}for(let K of G)if(K.type===3)K.children=zU(K.children);return G}function GU(U,z,G,H,K){if(!G||!z)return!1;if(z.nodeType===3)return!1;if(!b(U,G,H.firstChild(z),H,K)){if(U)console.warn("[DEBUG_HYDRATE] Could not copy DOM into virtual DOM, falling back to diff");while(H.firstChild(z))K.removeChild(z,H.lastChild(z));return!1}else if(U)console.info("[DEBUG_HYDRATE] Successfully prerendered page");return!0}function k(U,z,G){if(U)console.warn("[DEBUG_HYDRATE] VTree differed from node",z,G)}function qU(U,z){let G=N(U);return G?G.nextSibling:z}function b(U,z,G,H,K){switch(z.type){case 0:let X=z.mount(G.parentNode);if(z.componentId=X.componentId,z.child=X.componentTree,X.componentTree.parent=z,!b(U,z.child,G,H,K))return!1;break;case 3:z.children=zU(z.children);for(let $ of z.children){if(!G)return k(U,$,null),!1;if(!b(U,$,G,H,K))return!1;G=qU($,G)}break;case 2:if(G.nodeType!==3||z.text.trim()!==G.textContent.trim())return k(U,z,G),!1;z.domRef=G;break;case 1:if(G.nodeType!==1)return k(U,z,G),!1;z.domRef=G,z.children=zU(z.children),YU(G,z,K);let Y=G.firstChild;for(var Q=0;Q<z.children.length;Q++){let $=z.children[Q];if(!Y)return k(U,$,null),!1;if(!b(U,$,Y,H,K))return!1;Y=qU($,Y)}break}return!0}globalThis.miso={hydrationContext:_U,eventContext:j,drawingContext:UU,componentContext:WU,diff:A,hydrate:GU,version:m,callBlur:h,callFocus:y,callSelect:u,callSetSelectionRange:g,eventJSON:L,fetchCore:v,eventSourceConnect:d,eventSourceClose:p,websocketConnect:l,websocketClose:x,websocketSend:i,updateRef:s,inline:r,typeOf:c,mathRandom:t,getRandomValues:n,splitmix32:o,populateClass:a,delegateEvent:E,delegator:j.delegator,setDrawingContext:function(U){let z=globalThis[U].drawingContext,G=globalThis[U].eventContext,H=globalThis[U].componentContext;if(!z)console.error('Custom rendering engine ("drawingContext") is not defined at globalThis[name].drawingContext',U);if(!G)console.error('Custom event delegation ("eventContext") is not defined at globalThis[name].eventContext',U);if(!H)console.error('Custom component context ("componentContext") is not defined at globalThis[name].componentContext',U);globalThis.miso.drawingContext=z,globalThis.miso.eventContext=G,globalThis.miso.componentContext=H}};
diff --git a/miso.cabal b/miso.cabal
--- a/miso.cabal
+++ b/miso.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.2
 name:                miso
-version:             1.10.0.0
+version:             1.11.0.0
 category:            Web, Miso, Data Structures
 license:             BSD-3-Clause
 license-file:        LICENSE
diff --git a/src/Miso.hs b/src/Miso.hs
--- a/src/Miso.hs
+++ b/src/Miso.hs
@@ -73,7 +73,7 @@
 --   This is the templating function that is used to construct a new virtual DOM
 --   (or HTML if rendering on the server).
 --
--- * __update__: @'update' :: action -> 'Effect' parent model action@
+-- * __update__: @'update' :: action -> 'Effect' parent props model action@
 --   The 'update' function handles how the 'model' evolves over time in response
 --   to events that are raised by the application. This function takes any @action@,
 --   updating the @model@ and optionally introduces 'IO' into the system.
@@ -97,8 +97,8 @@
 --
 -- @
 -- data 'SomeComponent' parent
---   = forall model action . Eq model
---   => 'SomeComponent' ('Component' parent model action)
+--   = forall model action props . (Eq model, Eq props)
+--   => 'SomeComponent' props ('Component' parent model action)
 -- @
 --
 -- The smart constructors:
@@ -127,22 +127,23 @@
 -- import qualified Miso.Html.Property as HP
 -- -----------------------------------------------------------------------------
 --                        * - The type of the parent Component 'model'
---                        |     * - The type of the current Component's 'model'
---                        |     |    * - The type of the action that updates the 'model'
---                        |     |    |
--- counter :: 'Component' parent Int Action
+--                        |    * - The type of the parent Component 'props' accessible to the child
+--                        |    |     * - The type of the current Component's 'model'
+--                        |    |     |   * - The type of the action that updates the 'model'
+--                        |    |     |   |
+-- counter :: 'Component' ROOT () Int Action
 -- counter = 'vcomp' m u v
 --   where
 --     m :: Int
 --     m = 0
 --
---     u :: Action -> 'Effect' parent Int Action
+--     u :: Action -> 'Effect' ROOT () Int Action
 --     u = \\case
 --       Add -> 'this' += 1
 --       Subtract -> 'this' -= 1
 --
---     v :: Int -> 'View' Int Action
---     v x = 'vfrag'
+--     v :: () -> Int -> 'View' Int Action
+--     v _ x = 'vfrag'
 --       [ H.button_ [ HE.onClick Add, HP.id_ "add" ] [ "+" ]
 --       , text (ms x)
 --       , H.button_ [ HE.onClick Subtract, HP.id_ "subtract" ] [ "-" ]
@@ -203,7 +204,7 @@
 -- ('+>')
 --   :: forall child model action a . Eq child
 --   => 'MisoString'
---   -> 'Component' model child action
+--   -> 'Component' model () child action
 --   -> 'View' model a
 -- key '+>' vcomp = 'VComp' (Just (toKey key)) ('SomeComponent' vcomp)
 -- @
@@ -211,7 +212,7 @@
 -- Practically, using this combinator looks like:
 --
 -- @
--- view :: Int -> 'View' Int action
+-- view :: () -> Int -> 'View' Int action
 -- view x = 'div_' [ 'id_' "container" ] [ "counter" '+>' counter ]
 -- @
 --
@@ -233,7 +234,7 @@
 -- The 'App' type signature is a synonym for 'Component' 'ROOT'
 --
 -- @
--- type 'App' model action = 'Component' 'ROOT' model action
+-- type 'App' model action = 'Component' 'ROOT' () model action
 -- @
 --
 -- 'ROOT' is a type tag that encodes a 'Component' as top-level. Which means it has no @parent@, hence we mark @parent@ as 'ROOT'.
@@ -244,6 +245,149 @@
 --
 -- 'startApp' and 'miso' will always infer @parent@ as 'ROOT'.
 --
+-- = Props
+--
+-- Inspired by [React props](https://react.dev/learn/passing-props-to-a-component),
+-- @miso@ allows a parent 'Component' to pass read-only data down to a child 'Component'
+-- via a mechanism called /props/ (short for /properties/).
+--
+-- == Props vs. Component-local state
+--
+-- * __model__: Component-local state. It is owned and mutated exclusively by the 'Component'
+--   itself through its 'Miso.Types.update' function. No other 'Component' can write to it directly.
+--
+-- * __props__: Data /inherited/ from the @parent@ 'Component'. Props flow downward through
+--   the component hierarchy and are read-only from the child's perspective. The parent decides
+--   what props to pass at mount time; the child cannot mutate them. Props that change in a
+--   the parent cause the child to re-render.
+--
+-- This mirrors the distinction in React between component state (@useState@) and props
+-- received from above (@function MyComponent({ name }) { ... }@).
+--
+-- === When to use props
+--
+-- Props are best suited for /metadata/ — contextual or configuration data that the child needs
+-- to know about but should not own. Good examples: a user's display name, a theme token, a
+-- locale string, or a read-only identifier used to customise rendering.
+--
+-- If the data drives the child's own business logic — counters it increments, form fields it
+-- edits, async state it manages — that data belongs in the child's @model@ instead. Putting
+-- mutable business-logic state in @props@ would require the parent to own and thread through
+-- every change, creating unnecessary coupling. Prefer @props@ for \"what the child should
+-- know\" and @model@ for \"what the child should do\".
+--
+-- == Props in 'Miso.Types.view'
+--
+-- The 'Miso.Types.view' field of a 'Component' always takes @props@ as its first argument:
+--
+-- @
+-- view :: props -> model -> 'View' model action
+-- @
+--
+-- Top-level applications have no parent, so @props@ is always @()@:
+--
+-- @
+-- view :: () -> model -> 'View' model action
+-- view _props model = …
+-- @
+--
+-- == Props in 'Effect' \/ 'Miso.Types.update'
+--
+-- Use 'getProps' inside the 'Effect' monad to read the current value of @props@:
+--
+-- @
+-- update :: Action -> 'Effect' parent props Model Action
+-- update = \\case
+--   SomeAction -> do
+--     p <- 'getProps'
+--     'io_' ('consoleLog' (ms (show p)))
+-- @
+--
+-- Alternatively, use the 'Miso.Lens.view' combinator with the 'props' lens:
+--
+-- @
+-- update = \\case
+--   SomeAction -> do
+--     p <- 'Miso.Lens.view' 'props'
+--     …
+-- @
+--
+-- == 'ROOT' — the top-level 'Component'
+--
+-- When a 'Component' is passed to 'startApp' (or 'miso') it has no parent.
+-- The @parent@ type is specialized to 'ROOT' and @props@ is fixed to @()@:
+--
+-- @
+-- type 'App' model action = 'Component' 'ROOT' () model action
+-- @
+--
+-- Because there is no parent to inherit from, @props@ will always be @()@ for a
+-- root-level 'Component'. You can simply ignore the first argument in 'view' and
+-- skip 'getProps' in 'Miso.Types.update'.
+--
+-- == Passing props to a child 'Component'
+--
+-- Use 'mountWithProps_' (keyed) or 'mountWithProps' (unkeyed) in the parent's 'view' to
+-- mount a child and supply its props:
+--
+-- @
+-- 'mountWithProps_'
+--   :: ('Eq' child, 'Eq' props)
+--   => 'MisoString'
+--   -> props
+--   -> 'Component' parent props child action
+--   -> 'View' parent a
+-- @
+--
+-- == Example: child reading parent-supplied props
+--
+-- The following shows a parent 'Component' that maintains a greeting string in its
+-- @model@ and passes it as @props@ to a child 'Component'. The child renders the
+-- greeting and can also read it from within its 'Miso.Types.update' function.
+--
+-- @
+-- -----------------------------------------------------------------------------
+-- -- The props type: what the parent shares with the child
+-- newtype Greeting = Greeting 'MisoString' deriving ('Eq')
+-- -----------------------------------------------------------------------------
+-- -- Child component
+-- --
+-- --                   parent      props    model  action
+-- --                   |           |        |      |
+-- child :: 'Component' ParentModel Greeting ()     ChildAction
+-- child = 'vcomp' () updateChild viewChild
+--   where
+--     viewChild :: Greeting -> () -> 'View' () ChildAction
+--     viewChild (Greeting g) _ =
+--       'div_' [] [ 'text' ("Hello, " <> g <> "!") ]
+--
+--     updateChild :: ChildAction -> 'Effect' ParentModel Greeting () ChildAction
+--     updateChild = \\case
+--       ReadGreeting -> do
+--         Greeting g <- 'getProps'
+--         'io_' ('consoleLog' g)
+-- -----------------------------------------------------------------------------
+-- -- Parent component: owns the greeting, passes it to the child as props
+-- parent :: 'App' ParentModel ParentAction
+-- parent = 'vcomp' (ParentModel "World") 'noop' viewParent
+--   where
+--     viewParent :: () -> ParentModel -> 'View' ParentModel ParentAction
+--     viewParent _ (ParentModel g) =
+--       'mountWithProps_' "child" (Greeting g) child
+-- -----------------------------------------------------------------------------
+-- newtype ParentModel = ParentModel 'MisoString' deriving ('Eq')
+-- data ChildAction = ReadGreeting
+-- data ParentAction
+-- @
+--
+-- A few things to notice:
+--
+-- * The child's @parent@ type parameter is @ParentModel@. This must match the
+--   parent 'Component' @model@ type — 'mountWithProps_' enforces this at compile time.
+-- * 'getProps' inside the child's 'Miso.Types.update' yields a @Greeting@, not
+--   the full @ParentModel@. The child only sees what the parent explicitly chose to share.
+-- * The root 'App' always has @props ~ ()@; no extra plumbing is needed when calling 'startApp'.
+--
 -- = 'VComp' lifecycle hooks
 --
 -- 'Component' are mounted on the fly during diffing. All t'Component` are equipped with `mount` and `unmount` hooks. This allows the defining of custom actions that will be processed in response to lifecycle events.
@@ -271,7 +415,7 @@
 --
 -- data Action = Highlight DOMRef
 --
--- update :: Action -> 'Effect' parent model Action
+-- update :: Action -> 'Effect' parent props model Action
 -- update = \\case
 --   Highlight domRef -> 'io_' $ do
 --     ['js'| hljs.highlight(${domRef}) |]
@@ -344,7 +488,7 @@
 --   , 'li_' [ 'key_' "key-2" ] [ "b" ]
 --   , "key-3" '+>' counter
 --   , 'textKey' "key-4" "text here"
---   , vfrag_ "key-5" [ "foo", "bar" ]
+--   , 'vfrag_' "key-5" [ "foo", "bar" ]
 --   ]
 -- @
 --
@@ -420,7 +564,7 @@
 -- The 'Effect' type is defined as a 'RWS'.
 --
 -- @
--- type 'Effect' parent model action = 'RWS' ('ComponentInfo' parent) ['Schedule' action] model ()
+-- type 'Effect' parent props model action = 'RWS' ('ComponentInfo' parent props) ['Schedule' action] model ()
 -- @
 --
 -- * The 'Control.Monad.Reader' portion of 'Effect' is 'ComponentInfo'. 'ask', 'asks', 'Miso.Lens.view' can be used to access its fields.
@@ -470,6 +614,8 @@
 -- * 'broadcast'
 -- * 'mail'
 -- * 'mailParent'
+-- * 'mailChildren'
+-- * 'mailAncestors'
 --
 -- All t'Component' have a 'mailbox' that can receive messages (as t'Miso.JSON.Value') from other t'Component'.
 -- This is meant to be used with the 'checkMail' function. The 'mail' function allows a 'Component' to send a specific message (as 'Value') to another t'Component' via its t'ComponentId'.
@@ -585,7 +731,7 @@
 --
 -- import Miso.FFI.QQ ('js')
 --
--- update :: Action -> 'Effect' parent model Action
+-- update :: Action -> 'Effect' parent props model Action
 -- update = \\case
 --   Log msg -> io_ [js| console.log(${msg}) |]
 --
@@ -667,7 +813,7 @@
 -- A simple example of static prerendering would be an @index.html@ page with some HTML
 --
 -- @
--- echo "\<html\>\<head\>\<\/head\>\<body\>hello world\<\/body\>" > index.html
+-- echo "\<html\>\<head\>\<\/head\>\<body\>hello world\<\/body\>\<html\>" > index.html
 -- @
 --
 -- And a miso application that looks like:
@@ -720,6 +866,7 @@
   , parent
   , mailParent
   , mailChildren
+  , mailAncestors
   , broadcast
     -- ** Subscriptions
   , startSub
diff --git a/src/Miso/Effect.hs b/src/Miso/Effect.hs
--- a/src/Miso/Effect.hs
+++ b/src/Miso/Effect.hs
@@ -1,5 +1,6 @@
 -----------------------------------------------------------------------------
 {-# LANGUAGE CPP                        #-}
+{-# LANGUAGE FlexibleContexts           #-}
 {-# LANGUAGE FlexibleInstances          #-}
 -----------------------------------------------------------------------------
 {-# OPTIONS_GHC -fno-warn-orphans       #-}
@@ -52,14 +53,18 @@
   , componentInfoId
   -- *** Internal
   , runEffect
+  -- *** Props
+  , componentInfoProps
+  , props
+  , getProps
   ) where
 -----------------------------------------------------------------------------
 import           Control.Monad (void)
 import           Data.Foldable (for_)
-import           Control.Monad.RWS (RWS, put, tell, execRWS, censor)
+import           Control.Monad.RWS (RWS, put, tell, execRWS, censor, MonadReader)
 -----------------------------------------------------------------------------
-import           Miso.Lens
 import           Miso.DSL.FFI
+import           Miso.Lens
 -----------------------------------------------------------------------------
 -- | Smart constructor for t'ComponentInfo'
 mkComponentInfo
@@ -69,18 +74,20 @@
   -- ^ @parent@ 'ComponentId'
   -> DOMRef
   -- ^ 'DOMRef'
-  -> ComponentInfo parent
+  -> props
+  -> ComponentInfo parent props
 mkComponentInfo = ComponentInfo
 -----------------------------------------------------------------------------
 -- | This is the 'Reader r' in t'Miso.Effect'. Accessible via 'Control.Monad.Reader.ask'. It holds
 -- a phantom type for @parent@. This is used as a witness when calling the
 -- 'parent' function. It gives access to 'Component' metadata such as the 'DOMRef' the
 -- 'Component' was mounted on and the 'ComponentId' associated with it.
-data ComponentInfo parent
+data ComponentInfo parent props
   = ComponentInfo
   { _componentInfoId :: ComponentId
   , _componentInfoParentId :: ComponentId
   , _componentInfoDOMRef :: DOMRef
+  , _componentInfoProps :: props
   }
 -----------------------------------------------------------------------------
 -- | Lens for accessing the t'ComponentId' from t'ComponentInfo'.
@@ -93,7 +100,7 @@
 -- @
 --
 -- @since 1.9.0.0
-componentInfoId :: Lens (ComponentInfo parent) ComponentId
+componentInfoId :: Lens (ComponentInfo parent props) ComponentId
 componentInfoId = lens _componentInfoId $ \r x -> r { _componentInfoId = x }
 -----------------------------------------------------------------------------
 -- | Lens for accessing the parents's  t'ComponentId' from t'ComponentInfo'.
@@ -107,7 +114,7 @@
 -- @
 --
 -- @since 1.9.0.0
-componentInfoParentId :: Lens (ComponentInfo parent) ComponentId
+componentInfoParentId :: Lens (ComponentInfo parent props) ComponentId
 componentInfoParentId = lens _componentInfoParentId $ \r x -> r { _componentInfoParentId = x }
 -----------------------------------------------------------------------------
 -- | Lens for accessing the underlying t'Miso.Types.Component' t'DOMRef'.
@@ -120,9 +127,48 @@
 -- @
 --
 -- @since 1.9.0.0
-componentInfoDOMRef :: Lens (ComponentInfo parent) DOMRef
+componentInfoDOMRef :: Lens (ComponentInfo parent props) DOMRef
 componentInfoDOMRef = lens _componentInfoDOMRef $ \r x -> r { _componentInfoDOMRef = x }
 -----------------------------------------------------------------------------
+-- | Lens for accessing the underlying t'Miso.Types.Component' @props@.
+--
+-- @
+--   update = \case
+--     SomeAction -> do
+--       props <- view componentInfoProps
+--       someAction props
+-- @
+--
+-- @since 1.9.0.0
+componentInfoProps :: Lens (ComponentInfo parent props) props
+componentInfoProps = lens _componentInfoProps $ \r x -> r { _componentInfoProps = x }
+-----------------------------------------------------------------------------
+-- | Lens for accessing the underlying t'Miso.Types.Component' @props@.
+--
+-- This is a shorter convenience lens that is a synonynm for 'componentInfoProps'.
+-- See 'getProps' for usage in the 'Effect' monad.
+--
+-- @
+--   update = \case
+--     SomeAction ->
+--       someAction =<< view props
+-- @
+--
+props :: Lens (ComponentInfo parent props) props
+props = componentInfoProps
+-----------------------------------------------------------------------------
+-- | @props@ retrieval from within the 'Effect' monad.
+--
+-- @
+--   update = \case
+--     SomeAction -> do
+--       props <- getProps
+--       someAction props
+-- @
+--
+getProps :: MonadReader (ComponentInfo parent props) m => m props
+getProps = Miso.Lens.view props
+-----------------------------------------------------------------------------
 -- | 'ComponentId' of the current t'Miso.Types.Component'
 type ComponentId = Int
 -----------------------------------------------------------------------------
@@ -138,7 +184,7 @@
 -----------------------------------------------------------------------------
 -- | Smart constructor for an 'Effect' with exactly one action.
 infixl 0 <#
-(<#) :: model -> IO action -> Effect parent model action
+(<#) :: model -> IO action -> Effect parent props model action
 (<#) m action = put m >> tell [ async $ \f -> f =<< action ]
 -----------------------------------------------------------------------------
 async :: (Sink action -> IO ()) -> Schedule action
@@ -146,7 +192,7 @@
 -----------------------------------------------------------------------------
 -- | `Effect` smart constructor, flipped
 infixr 0 #>
-(#>) :: IO action -> model -> Effect parent model action
+(#>) :: IO action -> model -> Effect parent props model action
 (#>) = flip (<#)
 -----------------------------------------------------------------------------
 -- | Smart constructor for an 'Effect' with multiple 'IO' actions.
@@ -155,7 +201,7 @@
 batch
   :: [IO action]
   -- ^ Batch of 'IO' actions to execute
-  -> Effect parent model action
+  -> Effect parent props model action
 batch actions = sequence_
   [ tell [ async $ \f -> f =<< action ]
   | action <- actions
@@ -164,7 +210,7 @@
 -- | Like @batch@ but actions are discarded
 --
 -- @since 1.9.0.0
-batch_ :: [IO ()] -> Effect parent model action
+batch_ :: [IO ()] -> Effect parent props model action
 batch_ actions = sequence_
   [ tell [ async (const action) ]
   | action <- actions
@@ -202,7 +248,7 @@
 --   , ...
 --   }
 -- @
-type Effect parent model action = RWS (ComponentInfo parent) [Schedule action] model ()
+type Effect parent props model action = RWS (ComponentInfo parent props) [Schedule action] model ()
 -----------------------------------------------------------------------------
 -- | Represents a scheduled 'Effect' that is executed either synchronously
 -- or asynchronously.
@@ -222,8 +268,8 @@
 -----------------------------------------------------------------------------
 -- | Internal function used to unwrap an @Effect@
 runEffect
-    :: Effect parent model action
-    -> ComponentInfo parent
+    :: Effect parent props model action
+    -> ComponentInfo parent props
     -> model
     -> (model, [Schedule action])
 runEffect = execRWS
@@ -242,7 +288,7 @@
 sync
   :: IO action
   -- ^ 'IO' action to execute synchronously
-  -> Effect parent model action
+  -> Effect parent props model action
 sync action = tell [ Schedule Sync $ \f -> f =<< action ]
 -----------------------------------------------------------------------------
 -- | Like 'sync', except discards the result.
@@ -251,7 +297,7 @@
 sync_
   :: IO ()
   -- ^ 'IO' action to execute synchronously
-  -> Effect parent model action
+  -> Effect parent props model action
 sync_ action = tell [ Schedule Sync $ \_ -> action ]
 -----------------------------------------------------------------------------
 -- | Schedule a single 'IO' action for later execution.
@@ -263,7 +309,7 @@
 io
   :: IO action
   -- ^ 'IO' action to execute asynchronously
-  -> Effect parent model action
+  -> Effect parent props model action
 io action = withSink (action >>=)
 -----------------------------------------------------------------------------
 -- | Like 'io' but doesn't cause an action to be dispatched to
@@ -278,7 +324,7 @@
 io_
   :: IO ()
   -- ^ 'IO' action to execute asynchronously
-  -> Effect parent model action
+  -> Effect parent props model action
 io_ action = withSink (\_ -> void action)
 -----------------------------------------------------------------------------
 -- | Like 'io' but generalized to any instance of 'Foldable'
@@ -290,7 +336,7 @@
   :: Foldable f
   => IO (f action)
   -- ^ @actions@ executed in batch.
-  -> Effect parent model action
+  -> Effect parent props model action
 for actions = withSink $ \sink -> actions >>= flip for_ sink
 -----------------------------------------------------------------------------
 -- | Performs the given 'IO' action before all IO actions collected by the given
@@ -302,7 +348,7 @@
 -- @
 --
 -- @since 1.9.0.0
-beforeAll :: IO () -> Effect parent model action -> Effect parent model action
+beforeAll :: IO () -> Effect parent props model action -> Effect parent props model action
 beforeAll = modifyAllIO . (*>)
 -----------------------------------------------------------------------------
 -- | Performs the given 'IO' action after all IO actions collected by the given
@@ -312,7 +358,7 @@
 --
 -- > -- log that running the a websocket Effect completed
 -- > afterAll (consoleLog "Done running websocket effect") $ websocketConnectJSON OnConnect OnClose OnOpen OnError
-afterAll :: IO () -> Effect parent model action -> Effect parent model action
+afterAll :: IO () -> Effect parent props model action -> Effect parent props model action
 afterAll = modifyAllIO . (<*)
 -----------------------------------------------------------------------------
 -- | Modifies all 'IO' collected by the given Effect.
@@ -324,8 +370,8 @@
 -- expressions in an 'Effect'. For examples see 'beforeAll' and 'afterAll'.
 modifyAllIO
   :: (IO () -> IO ())
-  -> Effect parent model action
-  -> Effect parent model action
+  -> Effect parent props model action
+  -> Effect parent props model action
 modifyAllIO f = censor $ \actions ->
   [ Schedule x (f <$> action)
   | Schedule x action <- actions
@@ -339,14 +385,14 @@
 -- to turn events into actions.
 --
 -- @
--- 'update' FetchJSON = 'withSink' $ \sink -> getJSON (sink . ReceivedJSON) (sink . HandleError)
+-- 'update' FetchJSON = 'withSink' $ \\sink -> getJSON (sink . ReceivedJSON) (sink . HandleError)
 -- @
 --
 -- @since 1.9.0.0
 withSink
   :: (Sink action -> IO ())
   -- ^ Callback function that provides access to the underlying 'Sink'.
-  -> Effect parent model action
+  -> Effect parent props model action
 withSink f = tell [ async f ]
 -----------------------------------------------------------------------------
 -- | Issue a new @action@ to be processed by 'Miso.Types.update'.
@@ -364,14 +410,14 @@
 issue
   :: action
   -- ^ @action@ to raise
-  -> Effect parent model action
+  -> Effect parent props model action
 issue action = tell [ async $ \f -> f action ]
 -----------------------------------------------------------------------------
 -- | Helper for t'Miso.Types.Component' construction, when you want to ignore the 'Miso.Types.update'
 -- function temporarily, or permanently.
 --
 -- @since 1.9.0.0
-noop :: action -> Effect parent model action
+noop :: action -> Effect parent props model action
 noop = const (pure ())
 -----------------------------------------------------------------------------
 -- | Type to indicate if effects should be handled asynchronously
diff --git a/src/Miso/EventSource.hs b/src/Miso/EventSource.hs
--- a/src/Miso/EventSource.hs
+++ b/src/Miso/EventSource.hs
@@ -42,7 +42,7 @@
   -- ^ onMessage
   -> (MisoString -> action)
   -- ^ onError
-  -> Effect parent model action
+  -> Effect parent props model action
 connectText = eventSourceConnectText
 -----------------------------------------------------------------------------
 -- | <https://developer.mozilla.org/en-US/docs/Web/API/EventSource>
@@ -55,12 +55,12 @@
   -- ^ onMessage
   -> (MisoString -> action)
   -- ^ onError
-  -> Effect parent model action
+  -> Effect parent props model action
 connectJSON = eventSourceConnectJSON
 -----------------------------------------------------------------------------
 -- | <https://developer.mozilla.org/en-US/docs/Web/API/EventSource/close>
 close
   :: EventSource
-  -> Effect parent model action
+  -> Effect parent props model action
 close = eventSourceClose
 -----------------------------------------------------------------------------
diff --git a/src/Miso/Fetch.hs b/src/Miso/Fetch.hs
--- a/src/Miso/Fetch.hs
+++ b/src/Miso/Fetch.hs
@@ -100,7 +100,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 getJSON url headers_ successful errorful =
   withSink $ \sink ->
     FFI.fetch url "GET" Nothing jsonHeaders
@@ -141,7 +141,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 postJSON url body_ headers_ successful errorful =
   withSink $ \sink -> do
     bodyVal <- toJSVal (encode body_)
@@ -165,7 +165,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 postJSON' url body_ headers_ successful errorful =
   withSink $ \sink -> do
     bodyVal <- toJSVal (encode body_)
@@ -207,7 +207,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 putJSON url body_ headers_ successful errorful =
   withSink $ \sink -> do
     bodyVal <- toJSVal (encode body_)
@@ -229,7 +229,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 getText url headers_ successful errorful =
   withSink $ \sink ->
     FFI.fetch url "GET" Nothing textHeaders_
@@ -252,7 +252,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 postText url body_ headers_ successful errorful =
   withSink $ \sink -> do
     bodyVal <- toJSVal (encode body_)
@@ -276,7 +276,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 putText url imageBody headers_ successful errorful =
   withSink $ \sink -> do
     body_ <- toJSVal imageBody
@@ -298,7 +298,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 getBlob url headers_ successful errorful =
   withSink $ \sink ->
     FFI.fetch url "GET" Nothing blobHeaders_
@@ -321,7 +321,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 postBlob url body_ headers_ successful errorful =
   withSink $ \sink -> do
     bodyVal <- toJSVal body_
@@ -345,7 +345,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 putBlob url imageBody headers_ successful errorful =
   withSink $ \sink -> do
     body_ <- toJSVal imageBody
@@ -367,7 +367,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 getFormData url headers_ successful errorful =
   withSink $ \sink ->
     FFI.fetch url "GET" Nothing formDataHeaders_
@@ -390,7 +390,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action 
 postFormData url body_ headers_ successful errorful =
   withSink $ \sink -> do
     bodyVal <- toJSVal body_
@@ -414,7 +414,7 @@
   -- ^ successful callback
   -> (Response error  -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 putFormData url imageBody headers_ successful errorful =
   withSink $ \sink -> do
     body_ <- toJSVal imageBody
@@ -436,7 +436,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 getArrayBuffer url headers_ successful errorful =
   withSink $ \sink ->
     FFI.fetch url "GET" Nothing arrayBufferHeaders_
@@ -459,7 +459,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 postArrayBuffer url body_ headers_ successful errorful =
   withSink $ \sink -> do
     bodyVal <- toJSVal body_
@@ -483,7 +483,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 putArrayBuffer url arrayBuffer_ headers_ successful errorful =
   withSink $ \sink -> do
     body_ <- toJSVal arrayBuffer_
@@ -505,7 +505,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 getUint8Array url headers_ successful errorful =
   withSink $ \sink ->
     FFI.fetch url "GET" Nothing uint8ArrayHeaders_
@@ -528,7 +528,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 postUint8Array url body_ headers_ successful errorful =
   withSink $ \sink -> do
     bodyVal <- toJSVal body_
@@ -552,7 +552,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 putUint8Array url uint8Array_ headers_ successful errorful =
   withSink $ \sink -> do
     body_ <- toJSVal uint8Array_
@@ -576,7 +576,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 postImage url body_ headers_ successful errorful =
   withSink $ \sink -> do
     bodyVal <- toJSVal body_
@@ -598,7 +598,7 @@
   -- ^ successful callback
   -> (Response error -> action)
   -- ^ errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 putImage url imageBody headers_ successful errorful =
   withSink $ \sink -> do
     body_ <- toJSVal imageBody
diff --git a/src/Miso/Html/Render.hs b/src/Miso/Html/Render.hs
--- a/src/Miso/Html/Render.hs
+++ b/src/Miso/Html/Render.hs
@@ -97,7 +97,7 @@
   , "truespeed"
   ]
 ----------------------------------------------------------------------------
-renderBuilder :: Miso.Types.View m a -> Builder
+renderBuilder :: forall m a . Miso.Types.View m a -> Builder
 renderBuilder (VText _ "")    = fromMisoString " "
 renderBuilder (VText _ s)     = fromMisoString s
 renderBuilder (VNode _ "doctype" [] []) = "<!doctype html>"
@@ -131,13 +131,13 @@
               , ns == MATHML
               ]
 
-renderBuilder (VComp _ (SomeComponent vcomp_)) =
+renderBuilder (VComp _ (SomeComponent props vcomp_)) =
   foldMap renderBuilder vkids
     where
 #ifdef SSR
-      vkids = [ unsafeCoerce $ (view vcomp_) $ getInitialComponentModel vcomp_ ]
+      vkids = [ unsafeCoerce $ (view vcomp_) props (getInitialComponentModel vcomp_) ]
 #else
-      vkids = [ unsafeCoerce $ (view vcomp_) (model vcomp_) ]
+      vkids = [ unsafeCoerce $ (view vcomp_) props (model vcomp_) ]
 #endif
 renderBuilder (VFrag _ kids) = foldMap renderBuilder kids
 ----------------------------------------------------------------------------
diff --git a/src/Miso/Navigator.hs b/src/Miso/Navigator.hs
--- a/src/Miso/Navigator.hs
+++ b/src/Miso/Navigator.hs
@@ -52,7 +52,7 @@
   -- ^ Successful callback
   -> (JSVal -> action)
   -- ^ Errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 getUserMedia UserMedia {..} successful errorful =
   withSink $ \sink ->
     FFI.getUserMedia audio video
@@ -70,7 +70,7 @@
   -- ^ Successful callback
   -> (JSVal -> action)
   -- ^ Errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 copyClipboard txt successful errorful =
   withSink $ \sink ->
     FFI.copyClipboard txt
@@ -84,7 +84,7 @@
 isOnLine
   :: (Bool -> action)
   -- ^ Successful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 isOnLine action = io (action <$> FFI.isOnLine)
 -----------------------------------------------------------------------------
 -- | Type for dealing with 'navigator.mediaDevices.getUserMedia'
@@ -107,7 +107,7 @@
 geolocation
   :: (Geolocation -> action)
   -> (GeolocationError -> action)
-  -> Effect parent model action
+  -> Effect parent props model action
 geolocation successful errorful = do
   withSink $ \sink ->
     FFI.geolocation
diff --git a/src/Miso/Runtime.hs b/src/Miso/Runtime.hs
--- a/src/Miso/Runtime.hs
+++ b/src/Miso/Runtime.hs
@@ -51,6 +51,7 @@
   , parent
   , mailParent
   , mailChildren
+  , mailAncestors
   -- ** WebSocket
   , websocketConnect
   , websocketConnectJSON
@@ -150,18 +151,21 @@
 -----------------------------------------------------------------------------
 -- | Helper function to abstract out initialization of t'Miso.Types.Component' between top-level API functions.
 initialize
-  :: (Eq parent, Eq model)
+  :: (Eq parent, Eq model, Eq props)
   => Events
   -> ComponentId
   -> Hydrate
   -> Bool
   -- ^ Is the root node being rendered?
-  -> Component parent model action
+  -> props
+  -- ^ Initial props for this component
+  -> Component parent props model action
   -> IO DOMRef
   -- ^ Callback function is used for obtaining the t'Miso.Types.Component' 'DOMRef'.
-  -> IO (ComponentState parent model action)
-initialize events _componentParentId hydrate isRoot comp@Component {..} getComponentMountPoint = do
+  -> IO (ComponentState parent props model action)
+initialize events _componentParentId hydrate isRoot initialProps comp@Component {..} getComponentMountPoint = do
   _componentId <- freshComponentId
+  let _componentProps = initialProps
   let
     _componentSink = \action -> liftIO $ do
       atomicModifyIORef' globalQueue (\q -> (enqueue _componentId action q, ()))
@@ -192,7 +196,10 @@
       putMVar frame =<< fromJSValUnchecked jsval
 
   let _componentDraw = \newModel -> do
-        newVTree <- buildVTree events _componentParentId _componentId Draw _componentSink logLevel (view newModel)
+        props <- (^. componentProps) . (IM.! _componentId) <$> readIORef components
+        newVTree <-
+          buildVTree events _componentParentId _componentId Draw
+            _componentSink logLevel (view props newModel)
         oldVTree <- liftIO (readIORef _componentVTree)
         _frame <- requestAnimationFrame rAFCallback
         _timestamp :: Double <- takeMVar frame
@@ -201,8 +208,8 @@
         liftIO (atomicWriteIORef _componentVTree newVTree)
         FFI.flush
 
-  let _componentApplyActions = \(actions :: [action]) model_ comps -> do
-        let info = ComponentInfo _componentId _componentParentId _componentDOMRef
+  let _componentApplyActions = \(actions :: [action]) model_ comps props -> do 
+        let info = ComponentInfo _componentId _componentParentId _componentDOMRef props
         List.foldl' (\(vcomps, m, ss, dirtySet) a ->
           case runEffect (update a) info m of
             (n, sss) ->
@@ -306,6 +313,13 @@
   forever $ do
     getBatch >>= \case
       Nothing -> wait globalWaiter
+      Just (vcompId, [])
+        | vcompId < 0 -> do
+            -- props propagation, negated 'ComponentId' indicates render-phase only.
+            vcomps <- liftIO (readIORef components)
+            forM_ (IM.lookup (negate vcompId) vcomps) $ \ComponentState {..} ->
+              _componentDraw _componentModel
+                             
       Just (vcompId, actions) -> do
         mounted <- isMounted vcompId
         when mounted (run vcompId actions)
@@ -322,7 +336,7 @@
       (updatedModel, schedules, dirtySet, ComponentState{..}) <- do
         atomicModifyIORef' components $ \vcomps -> do
           let cs@ComponentState {..} = vcomps IM.! vcompId
-          case _componentApplyActions events _componentModel vcomps of
+          case _componentApplyActions events _componentModel vcomps _componentProps of
             (x, updatedModel, schedules, dirtySet) ->
               (x, (updatedModel, schedules, dirtySet, cs))
       forM_ schedules $ \case
@@ -358,7 +372,7 @@
 -- Auxiliary function
 modifyComponent
   :: ComponentId
-  -> State (ComponentState parent model action) a
+  -> State (ComponentState parent props model action) a
   -> IO ()
 modifyComponent vcompId go = liftIO $ do
   atomicModifyIORef' components $ \vcomps ->
@@ -370,21 +384,21 @@
 ----------------------------------------------------------------------------
 propagate
   :: ComponentId
-  -> IntMap (ComponentState p m a)
-  -> (IntMap (ComponentState p m a), ComponentIds)
+  -> IntMap (ComponentState p props m a)
+  -> (IntMap (ComponentState p props m a), ComponentIds)
 propagate vcompId vcomps =
   let dfsState = execState synch (dfs vcomps vcompId)
   in (_state dfsState, _visited dfsState)
 -----------------------------------------------------------------------------
 -- | Create an empty DFS state
-dfs :: IntMap (ComponentState p m a) -> ComponentId -> DFS p m a
+dfs :: IntMap (ComponentState p props m a) -> ComponentId -> DFS p props m a
 dfs cs vcompId = DFS cs mempty (pure vcompId)
 -----------------------------------------------------------------------------
 type ComponentIds = IntSet
 -----------------------------------------------------------------------------
-data DFS p m a
+data DFS p props m a
   = DFS
-  { _state :: IntMap (ComponentState p m a)
+  { _state :: IntMap (ComponentState p props m a)
     -- ^ global component state to alter
   , _visited :: ComponentIds
     -- ^ visited set
@@ -392,21 +406,21 @@
     -- ^ neighbors queue
   }
 -----------------------------------------------------------------------------
-type Synch p m a x = State (DFS p m a) x
+type Synch p props m a x = State (DFS p props m a) x
 -----------------------------------------------------------------------------
-visited :: Lens (DFS p m a) (ComponentIds)
+visited :: Lens (DFS p props m a) (ComponentIds)
 visited = lens _visited $ \r x -> r { _visited = x }
 -----------------------------------------------------------------------------
-state :: Lens (DFS p m a) (IntMap (ComponentState p m a))
+state :: Lens (DFS p props m a) (IntMap (ComponentState p props m a))
 state = lens _state $ \r x -> r { _state = x }
 -----------------------------------------------------------------------------
-stack :: Lens (DFS p m a) [ComponentId]
+stack :: Lens (DFS p props m a) [ComponentId]
 stack = lens _stack $ \r x -> r { _stack = x }
 -----------------------------------------------------------------------------
-synch :: Synch p m a ()
+synch :: Synch p props m a ()
 synch = mapM_ go =<< pop
   where
-    go :: ComponentState p m a -> Synch p m a ()
+    go :: ComponentState p props m a -> Synch p props m a ()
     go cs = do
       seen <- IS.member (cs ^. componentId) <$> use visited
       when (not seen) $ do
@@ -416,10 +430,10 @@
         synch
 -----------------------------------------------------------------------------
 propagateChildren
-  :: forall p m a
-   . ComponentState p m a
+  :: forall p props m a
+   . ComponentState p props m a
   -> ComponentIds
-  -> Synch p m a ()
+  -> Synch p props m a ()
 propagateChildren currentState childComponents = do
   forM_ (IS.toList childComponents) $ \childId -> do
     childState <- unsafeCoerce (IM.! childId) <$> use state
@@ -434,9 +448,9 @@
       visit childId
     where
       process
-        :: ComponentState m child a
+        :: ComponentState m props child a
         -> Binding m child
-        -> Synch p m a (ComponentState m child a)
+        -> Synch p props m a (ComponentState m props child a)
       process childState = \case
         ParentToChild getCurrentField setChildField -> do
           let currentChildModel = childState ^. componentModel
@@ -452,10 +466,10 @@
           pure childState
 -----------------------------------------------------------------------------
 propagateParent
-  :: forall p m a
-   . ComponentState p m a
+  :: forall p props m a
+   . ComponentState p props m a
   -> ComponentId
-  -> Synch p m a ()
+  -> Synch p props m a ()
 propagateParent currentState parentId_ =
   IM.lookup parentId_ <$> use state >>= mapM_ \case
     parentState -> do
@@ -470,9 +484,9 @@
         visit parentId_
   where
     process
-      :: ComponentState x p a
+      :: ComponentState x props p a
       -> Binding p m
-      -> Synch p m a (ComponentState x p a)
+      -> Synch p props m a (ComponentState x props p a)
     process parentState = \case
       ChildToParent setParentField getCurrentField -> do
         let currentParentModel = parentState ^. componentModel
@@ -487,13 +501,13 @@
       _ ->
         pure parentState
 -----------------------------------------------------------------------------
-markVisited :: ComponentId -> Synch p m a ()
+markVisited :: ComponentId -> Synch p props m a ()
 markVisited vcompId = visited.at vcompId ?= ()
 -----------------------------------------------------------------------------
-visit :: ComponentId -> Synch p m a ()
+visit :: ComponentId -> Synch p props m a ()
 visit vcompId = stack %= (vcompId:)
 -----------------------------------------------------------------------------
-pop :: Synch p m a (Maybe (ComponentState p m a))
+pop :: Synch p props m a (Maybe (ComponentState p props m a))
 pop = use stack >>= \case
   [] ->
     pure Nothing
@@ -502,19 +516,20 @@
     use (state . at x)
 -----------------------------------------------------------------------------
 initialDraw
-  :: Eq m
+  :: (Eq m, Eq props)
   => m
   -> Events
   -> Hydrate
   -> Bool
-  -> Component p m a
-  -> ComponentState p m a
+  -> Component p props m a
+  -> ComponentState p props m a
   -> IO ()
 initialDraw initializedModel events hydrate isRoot Component {..} ComponentState {..} = do
 #ifdef BENCH
   start <- FFI.now
 #endif
-  vtree <- buildVTree events _componentParentId _componentId hydrate _componentSink logLevel (view initializedModel)
+  vtree <- buildVTree events _componentParentId _componentId hydrate _componentSink logLevel
+    (view _componentProps initializedModel)
 #ifdef BENCH
   end <- FFI.now
   when isRoot $ FFI.consoleLog $ ms (printf "buildVTree: %.3f ms" (end - start) :: String)
@@ -533,7 +548,7 @@
             else do
               newTree <-
                 buildVTree events _componentParentId _componentId Draw
-                  _componentSink logLevel (view initializedModel)
+                  _componentSink logLevel (view _componentProps initializedModel)
               Diff.diff Nothing (Just newTree) _componentDOMRef
               liftIO (atomicWriteIORef _componentVTree newTree)
         else
@@ -581,6 +596,13 @@
   q & queue %~ IM.insertWith (flip (<>)) vcompId (S.singleton action)
     & queueSchedule %~ (S.|> vcompId)
 -----------------------------------------------------------------------------
+-- | Used to fast track to render phase, bypassing commit phase. Used in 'props'
+-- feature.
+enqueueSchedule :: ComponentId -> IO ()
+enqueueSchedule vcompId =
+  atomicModifyIORef' globalQueue $ \q ->
+     (q & queueSchedule %~ (S.|> negate vcompId), ())
+-----------------------------------------------------------------------------
 -- | Case on queue schedule, get first item, span on the rest of queueSchedule, get length.
 -- set schedule with whatever remains.
 --
@@ -596,8 +618,9 @@
     S.Empty -> Nothing
     sched@(vcompId S.:<| leftover) ->
       case q ^. queue . at vcompId of
-        Nothing -> -- dmj: if unmount occurred, just pop the schedule
-          Just (vcompId, [], q & queueSchedule .~ leftover)
+        Nothing ->
+          let (_, remaining) = S.spanl (== vcompId) sched
+          in Just (vcompId, [], q & queueSchedule .~ remaining)
         Just actions ->
           case S.spanl (==vcompId) sched of
             (scheduled, remaining) ->
@@ -632,27 +655,30 @@
 {-# NOINLINE globalQueue #-}
 globalQueue = unsafePerformIO (newIORef emptyQueue)
 -----------------------------------------------------------------------------
-componentId :: Lens (ComponentState parent model action) ComponentId
+componentId :: Lens (ComponentState parent props model action) ComponentId
 componentId = lens _componentId $ \record field -> record { _componentId = field }
 -----------------------------------------------------------------------------
-parentId :: Lens (ComponentState parent model action) ComponentId
+parentId :: Lens (ComponentState parent props model action) ComponentId
 parentId = lens _componentParentId $ \record field -> record { _componentParentId = field }
 -----------------------------------------------------------------------------
-children :: Lens (ComponentState parent model action) (ComponentIds)
+children :: Lens (ComponentState parent props model action) (ComponentIds)
 children = lens _componentChildren $ \record field -> record { _componentChildren = field }
 -----------------------------------------------------------------------------
-componentTopics :: Lens (ComponentState parent model action) (Map MisoString (Value -> IO ()))
+componentTopics :: Lens (ComponentState parent props model action) (Map MisoString (Value -> IO ()))
 componentTopics = lens _componentTopics $ \record field -> record { _componentTopics = field }
 -----------------------------------------------------------------------------
-isDirty :: Lens (ComponentState parent model action) Bool
+isDirty :: Lens (ComponentState parent props model action) Bool
 isDirty = lens _componentIsDirty $ \record field -> record { _componentIsDirty = field }
 -----------------------------------------------------------------------------
-componentModel :: Lens (ComponentState parent model action) model
+componentModel :: Lens (ComponentState parent props model action) model
 componentModel = lens _componentModel $ \record field -> record { _componentModel = field }
 -----------------------------------------------------------------------------
-componentBindings :: Lens (ComponentState p m a) [Binding p m]
+componentBindings :: Lens (ComponentState p props m a) [Binding p m]
 componentBindings = lens _componentBindings $ \record field -> record { _componentBindings = field }
 -----------------------------------------------------------------------------
+componentProps :: Lens (ComponentState parent props model action) props
+componentProps = lens _componentProps $ \record field -> record { _componentProps = field }
+-----------------------------------------------------------------------------
 -- | Hydrate avoids calling @diff@, and instead calls @hydrate@
 -- 'Draw' invokes 'Miso.Diff.diff'
 data Hydrate
@@ -661,12 +687,14 @@
   deriving (Show, Eq)
 -----------------------------------------------------------------------------
 -- | t'Miso.Types.Component' state, data associated with the lifetime of a t'Miso.Types.Component'
-data ComponentState parent model action
+data ComponentState parent props model action
   = ComponentState
   { _componentId :: ComponentId
   -- ^ The ID of the current t'Miso.Types.Component'
   , _componentParentId :: ComponentId
   -- ^ The ID of the t'Miso.Types.Component''s parent
+  , _componentProps :: props
+  -- ^ The current props passed to this t'Miso.Types.Component'
   , _componentSubThreads :: IORef (Map MisoString ThreadId)
   -- ^ Mapping of all 'Sub' in use by t'Miso.Types.Component'
   , _componentDOMRef :: DOMRef
@@ -694,12 +722,14 @@
   , _componentApplyActions
       :: [action]
       -> model
-      -> IntMap (ComponentState parent model action)
-      -> (IntMap (ComponentState parent model action), model, [Schedule action], ComponentIds)
+      -> IntMap (ComponentState parent props model action)
+      -> props
+      -> (IntMap (ComponentState parent props model action), model, [Schedule action], ComponentIds)
   -- ^ t'Miso.Types.Component' actions application
   , _componentTopics :: Map MisoString (Value -> IO ())
   -- ^ t'Miso.Types.Component' topics using for Pub Sub async communication.
   , _componentChildren :: ComponentIds
+  -- ^ 'IntSet' of children t'Miso.Types.ComponentId'
   }
 -----------------------------------------------------------------------------
 -- | A @Topic@ represents a place to send and receive messages. @Topic@ is used to facilitate
@@ -804,7 +834,7 @@
   => Topic message
   -> (message -> action)
   -> (MisoString -> action)
-  -> Effect parent model action
+  -> Effect parent props model action
 subscribe (Topic topicName) successful errorful = do
   ComponentInfo {..} <- ask
   withSink $ \sink ->
@@ -851,7 +881,7 @@
 -- See 'subscribe' for more use.
 --
 -- @since 1.9.0.0
-unsubscribe :: Topic message -> Effect parent model action
+unsubscribe :: Topic message -> Effect parent props model action
 unsubscribe (Topic topicName) = do
   ComponentInfo {..} <- ask
   io_ $ modifyComponent _componentInfoId $ do
@@ -880,11 +910,7 @@
 --   , button_ [ onClick AddOne ] [ "+" ]
 --   , button_ [ onClick SubtractOne ] [ "-" ]
 --   , component_ (client_ "client 1")
---     [ onMountedWith Mount
---     ]
 --   , component_ (client_ "client 2")
---     [ onMountedWith Mount
---     ]
 --   ] where
 --       update_ :: Action -> Effect parent () Action
 --       update_ = \case
@@ -944,7 +970,7 @@
 --
 -- This is a global t'Miso.Types.Component' @Map@ that holds the state of all currently
 -- mounted t'Miso.Types.Component's
-components :: IORef (IntMap (ComponentState parent model action))
+components :: IORef (IntMap (ComponentState parent props model action))
 {-# NOINLINE components #-}
 components = unsafePerformIO (newIORef mempty)
 -----------------------------------------------------------------------------
@@ -958,14 +984,14 @@
 -----------------------------------------------------------------------------
 -- | Drains the event queue before unmounting, executed synchronously.
 drain
-  :: ComponentState parent model action
+  :: ComponentState parent props model action
   -> IO ()
 drain ComponentState {..} = do
   drainQueueAt _componentId >>= \case
     [] -> pure ()
     actions -> do
        vcomps <- readIORef components
-       case _componentApplyActions actions _componentModel vcomps of
+       case _componentApplyActions actions _componentModel vcomps _componentProps of
          (newVComps, _, schedules, _) -> do
            forM_ schedules $ \case
              -- dmj: process all actions synchronously during unmount
@@ -978,13 +1004,13 @@
            atomicWriteIORef components newVComps
 -----------------------------------------------------------------------------
 -- | Post unmount call to drop the <style> and <script> in <head>
-unloadScripts :: ComponentState parent model action -> IO ()
+unloadScripts :: ComponentState parent props model action -> IO ()
 unloadScripts ComponentState {..} = do
   head_ <- FFI.getHead
   forM_ _componentScripts (FFI.removeChild head_)
 -----------------------------------------------------------------------------
 -- | Helper to drop all lifecycle and mounting hooks if defined.
-freeLifecycleHooks :: ComponentState parent model action -> IO ()
+freeLifecycleHooks :: ComponentState parent props model action -> IO ()
 freeLifecycleHooks ComponentState {..} = do
   VTree (Object comp) <- liftIO (readIORef _componentVTree)
   mapM_ freeFunction =<< fromJSVal =<< comp ! ("mount" :: MisoString)
@@ -992,7 +1018,7 @@
 -----------------------------------------------------------------------------
 -- | Helper function for cleanly destroying a t'Miso.Types.Component'
 unmountComponent
-  :: ComponentState parent model action
+  :: ComponentState parent props model action
   -> IO ()
 unmountComponent cs@ComponentState {..} = do
   liftIO (mapM_ killThread =<< readIORef _componentSubThreads)
@@ -1001,9 +1027,10 @@
   finalizeEventSources _componentId
   unloadScripts cs
   freeLifecycleHooks cs
-  liftIO $ modifyComponent _componentParentId $ do
-    children.at _componentId .= Nothing
-  liftIO $ atomicModifyIORef' components $ \m -> (IM.delete _componentId m, ())
+  liftIO $ do
+    modifyComponent _componentParentId $ do
+      children.at _componentId .= Nothing
+    atomicModifyIORef' components $ \m -> (IM.delete _componentId m, ())
   FFI.unmountComponent _componentId
 -----------------------------------------------------------------------------
 resetComponentState :: IO () -> IO ()
@@ -1033,12 +1060,12 @@
   -> View model action
   -> IO VTree
 buildVTree events_ parentId_ vcompId hydrate snk logLevel_ = \case
-  VComp maybeKey (SomeComponent app) -> do
+  VComp maybeKey (SomeComponent newProps app) -> do
     vcomp_ <- create
 
     mountCallback <- do
       syncCallback1' $ \parent_ -> do
-        ComponentState {..} <- initialize events_ vcompId hydrate False app (pure parent_)
+        ComponentState {..} <- initialize events_ vcompId hydrate False newProps app (pure parent_)
         modifyComponent vcompId (children %= IS.insert _componentId)
         vtree <- toJSVal =<< readIORef _componentVTree
         FFI.set "parent" vcomp_ (Object vtree)
@@ -1056,6 +1083,18 @@
             forM_ (unmount app) (_componentSink componentState)
             unmountComponent componentState
 
+    -- When props are present, install a diffProps callback.
+    -- Comparison happens in Haskell against _componentLastProps — no round-trip.
+    -- TypeScript calls diffProps() unconditionally; Haskell decides whether to dispatch.
+    diffPropsCallback <- toJSVal =<< do
+      syncCallback $ do
+        componentId_ <- fromJSValUnchecked =<< vcomp_ ! ("componentId" :: MisoString)
+        currentProps <- _componentProps . (IM.! componentId_) <$> readIORef components
+        when (currentProps /= newProps) $ do
+          modifyComponent componentId_ (componentProps .= newProps)
+          enqueueSchedule componentId_
+
+    FFI.set "diffProps" diffPropsCallback vcomp_
     FFI.set "child" jsNull vcomp_
     forM_ maybeKey (\key -> FFI.set "key" key vcomp_)
     FFI.set "mount" mountCallback vcomp_
@@ -1161,7 +1200,7 @@
         FFI.set k v (Object cssObj)
 -----------------------------------------------------------------------------
 -- | Registers components in the global state
-registerComponent :: MonadIO m => ComponentState parent model action -> m ()
+registerComponent :: MonadIO m => ComponentState parent props model action -> m ()
 registerComponent componentState = liftIO $
   atomicModifyIORef' components $ \cs ->
     (IM.insert (_componentId componentState) componentState cs, ())
@@ -1221,7 +1260,7 @@
   -- ^ The key used to track the 'Sub'
   -> Sub action
   -- ^ The 'Sub'
-  -> Effect parent model action
+  -> Effect parent props model action
 startSub subKey sub = do
   ComponentInfo {..} <- ask
   io_ $ do
@@ -1260,7 +1299,7 @@
   :: ToMisoString subKey
   => subKey
   -- ^ The key used to stop the 'Sub'
-  -> Effect parent model action
+  -> Effect parent props model action
 stopSub subKey = do
   vcompId <- asks _componentInfoId
   io_ $ do
@@ -1277,7 +1316,7 @@
 -- | Send any @ToJSON message => message@ to a t'Miso.Types.Component' mailbox, by 'ComponentId'
 --
 -- @
--- io_ $ mail componentId ("test message" :: MisoString) :: Effect parent model action
+-- io_ $ mail componentId ("test message" :: MisoString) :: Effect parent props model action
 -- @
 --
 -- @since 1.9.0.0
@@ -1300,7 +1339,7 @@
 -- | Send any @ToJSON message => message@ to the parent's t'Miso.Types.Component' mailbox
 --
 -- @
--- mailParent ("test message" :: MisoString) :: Effect parent model action
+-- mailParent ("test message" :: MisoString) :: Effect parent props model action
 -- @
 --
 -- @since 1.9.0.0
@@ -1308,15 +1347,42 @@
   :: ToJSON message
   => message
   -- ^ Message to send
-  -> Effect parent model action
+  -> Effect parent props model action
 mailParent msg = do
   ComponentInfo {..} <- ask
   io_ (mail _componentInfoParentId msg)
 -----------------------------------------------------------------------------
+-- | Send any @ToJSON message => message@ to all ancestor t'Miso.Types.Component' 'mailbox'.
+--
+-- This function walks the t'Miso.Types.Component' ancestor hierarchy, delivering mail
+-- along the way.
+--
+-- @
+-- mailAncestors ("test message" :: MisoString) :: Effect parent props model action
+-- @
+--
+-- @since 1.11.0.0
+mailAncestors
+  :: ToJSON message
+  => message
+  -- ^ Message to send
+  -> Effect parent props model action
+mailAncestors msg = do
+  ComponentInfo {..} <- ask
+  io_ (climb _componentInfoParentId)
+    where
+      climb vcompId = do
+        mail vcompId msg
+        IM.lookup vcompId <$> readIORef components >>= \case
+          Nothing -> pure ()
+          Just cs -> climb (_componentParentId cs)
+-----------------------------------------------------------------------------
 -- | Send any @ToJSON message => message@ to the children's t'Miso.Types.Component' mailbox
 --
+-- N.B. this is only relevant for immediate descendants (not all descendants).
+--
 -- @
--- mailChildren ("test message" :: MisoString) :: Effect parent model action
+-- mailChildren ("test message" :: MisoString) :: Effect parent props model action
 -- @
 --
 -- @since 1.9.0.0
@@ -1324,7 +1390,7 @@
   :: ToJSON message
   => message
   -- ^ Message to send
-  -> Effect parent model action
+  -> Effect parent props model action
 mailChildren msg = do
   ComponentInfo {..} <- ask
   io_ $ do
@@ -1368,7 +1434,7 @@
   -- ^ Successful callback
   -> action
   -- ^ Errorful callback
-  -> Effect parent model action
+  -> Effect parent props model action
 parent successful errorful = do
   ComponentInfo {..} <- ask
   withSink $ \sink -> do
@@ -1381,7 +1447,7 @@
 --
 -- @
 --
--- update :: action -> Effect parent model action
+-- update :: action -> Effect parent props model action
 -- update _ = broadcast (String "public service announcement")
 -- @
 --
@@ -1391,7 +1457,7 @@
   => ToJSON message
   => message
   -- ^ Message to broadcast to all other 'Component'
-  -> Effect parent model action
+  -> Effect parent props model action
 broadcast msg = do
   ComponentInfo {..} <- ask
   io_ $ do
@@ -1430,7 +1496,7 @@
   -- ^ onMessage
   -> (MisoString -> action)
   -- ^ onError
-  -> Effect parent model action
+  -> Effect parent props model action
 websocketConnectText url onOpen onClosed onMessage onError =
   websocketCore $ \webSocketId sink ->
     FFI.websocketConnect url
@@ -1454,7 +1520,7 @@
   -- ^ onMessage
   -> (MisoString -> action)
   -- ^ onError
-  -> Effect parent model action
+  -> Effect parent props model action
 websocketConnectBLOB url onOpen onClosed onMessage onError =
   websocketCore $ \webSocketId sink ->
     FFI.websocketConnect url
@@ -1478,7 +1544,7 @@
   -- ^ onMessage
   -> (MisoString -> action)
   -- ^ onError
-  -> Effect parent model action
+  -> Effect parent props model action
 websocketConnectArrayBuffer url onOpen onClosed onMessage onError =
   websocketCore $ \webSocketId sink ->
     FFI.websocketConnect url
@@ -1503,7 +1569,7 @@
   -- ^ onMessage
   -> (MisoString -> action)
   -- ^ onError
-  -> Effect parent model action
+  -> Effect parent props model action
 websocketConnectJSON url onOpen onClosed onMessage onError =
   websocketCore $ \webSocketId sink ->
     FFI.websocketConnect url
@@ -1532,7 +1598,7 @@
   -- ^ onMessage
   -> (MisoString -> action)
   -- ^ onError
-  -> Effect parent model action
+  -> Effect parent props model action
 websocketConnect url onOpen onClosed onMessage onError =
   websocketCore $ \webSocketId sink ->
     FFI.websocketConnect url
@@ -1552,7 +1618,7 @@
 -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket>
 websocketCore
   :: (WebSocket -> Sink action -> IO Socket)
-  -> Effect parent model action
+  -> Effect parent props model action
 websocketCore core = do
   ComponentInfo {..} <- ask
   withSink $ \sink -> do
@@ -1590,7 +1656,7 @@
           (IM.delete vcompId websockets, ())
 -----------------------------------------------------------------------------
 -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close>
-websocketClose :: WebSocket -> Effect parent model action
+websocketClose :: WebSocket -> Effect parent props model action
 websocketClose socketId = do
   ComponentInfo {..} <- ask
   io_ $ do
@@ -1617,7 +1683,7 @@
   :: ToJSON value
   => WebSocket
   -> Payload value
-  -> Effect parent model action
+  -> Effect parent props model action
 websocketSend socketId msg = do
   ComponentInfo {..} <- ask
   io_ $ do
@@ -1638,7 +1704,7 @@
 --
 -- If the t'WebSocket' identifier does not exist a 'CLOSED' is returned.
 --
-socketState :: WebSocket -> (SocketState -> action) -> Effect parent model action
+socketState :: WebSocket -> (SocketState -> action) -> Effect parent props model action
 socketState socketId callback = do
   ComponentInfo {..} <- ask
   withSink $ \sink -> do
@@ -1766,7 +1832,7 @@
   -- ^ onMessage
   -> (MisoString -> action)
   -- ^ onError
-  -> Effect parent model action
+  -> Effect parent props model action
 eventSourceConnectText url onOpen onMessage onError =
   eventSourceCore $ \eventSourceId sink -> do
     FFI.eventSourceConnect url
@@ -1789,7 +1855,7 @@
   -- ^ onMessage
   -> (MisoString -> action)
   -- ^ onError
-  -> Effect parent model action
+  -> Effect parent props model action
 eventSourceConnectJSON url onOpen onMessage onError =
   eventSourceCore $ \eventSourceId sink -> do
     FFI.eventSourceConnect url
@@ -1805,7 +1871,7 @@
 -- | <https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource>
 eventSourceCore
   :: (EventSource -> Sink action -> IO Socket)
-  -> Effect parent model action
+  -> Effect parent props model action
 eventSourceCore core = do
   ComponentInfo {..} <- ask
   withSink $ \sink -> do
@@ -1828,7 +1894,7 @@
       atomicModifyIORef' eventSourceConnectionIds (\x -> (x + 1, x))
 -----------------------------------------------------------------------------
 -- | <https://developer.mozilla.org/en-US/docs/Web/API/EventSource/close>
-eventSourceClose :: EventSource -> Effect parent model action
+eventSourceClose :: EventSource -> Effect parent props model action
 eventSourceClose socketId = do
   ComponentInfo {..} <- ask
   io_ $ do
@@ -1892,11 +1958,11 @@
   :: (Eq parent, Eq model)
   => Events
   -> Hydrate
-  -> Component parent model action
+  -> Component parent () model action
   -> IO ()
 initComponent events hydrate vcomp_@Component {..} = withJS $ do
   root <- Diff.mountElement (getMountPoint mountPoint)
-  void $ initialize events rootComponentId hydrate True vcomp_ (pure root)
+  void $ initialize events rootComponentId hydrate True () vcomp_ (pure root)
 #if __GLASGOW_HASKELL__ > 865
   flip labelThread "scheduler" =<< forkIO scheduler
 #else
diff --git a/src/Miso/Types.hs b/src/Miso/Types.hs
--- a/src/Miso/Types.hs
+++ b/src/Miso/Types.hs
@@ -66,6 +66,8 @@
   -- ** Component mounting
   , (+>)
   , mount_
+  , mountWithProps_
+  , mountWithProps
   -- ** Key combinators
   , keyed
   -- ** Fragment combinators
@@ -114,7 +116,7 @@
 import           Miso.CSS.Types (StyleSheet)
 -----------------------------------------------------------------------------
 -- | Application entry point
-data Component parent model action
+data Component parent props model action
   = Component
   { model :: model
   -- ^ Initial model
@@ -125,9 +127,9 @@
 #endif
   -- ^ Optional 'IO' to load component 'model' state, such as reading data from page.
   --   The resulting 'model' is only used during initial hydration, not on remounts.
-  , update :: action -> Effect parent model action
+  , update :: action -> Effect parent props model action
   -- ^ Updates model, optionally providing effects.
-  , view :: model -> View model action
+  , view :: props -> model -> View model action
   -- ^ Draws 'View'
   , subs :: [ Sub action ]
   -- ^ Subscriptions to run during application lifetime
@@ -228,11 +230,11 @@
 component
   :: model
   -- ^ model
-  -> (action -> Effect parent model action)
+  -> (action -> Effect parent props model action)
   -- ^ update
-  -> (model -> View model action)
+  -> (props -> model -> View model action)
   -- ^ view
-  -> Component parent model action
+  -> Component parent props model action
 component m u v = Component
   { model = m
   , hydrateModel = Nothing
@@ -254,11 +256,11 @@
 vcomp
   :: model
   -- ^ model
-  -> (action -> Effect parent model action)
+  -> (action -> Effect parent props model action)
   -- ^ update
-  -> (model -> View model action)
+  -> (props -> model -> View model action)
   -- ^ view
-  -> Component parent model action
+  -> Component parent props model action
 vcomp = component  
 -----------------------------------------------------------------------------
 -- | A top-level t'Miso.Types.Component' can have no @parent@.
@@ -274,7 +276,7 @@
 -- | A miso application is a top-level t'Miso.Types.Component', which has no @parent@.
 -- This is enforced by specializing the @parent@ type parameter to 'ROOT'.
 --
-type App model action = Component ROOT model action
+type App model action = Component ROOT () model action
 -----------------------------------------------------------------------------
 -- | Logging configuration for debugging Miso internals (useful to see if prerendering is successful)
 data LogLevel
@@ -309,8 +311,8 @@
 -----------------------------------------------------------------------------
 -- | Existential wrapper allowing nesting of t'Miso.Types.Component' in t'Miso.Types.Component'
 data SomeComponent parent
-   = forall model action . Eq model
-  => SomeComponent (Component parent model action)
+   = forall model action props . (Eq model, Eq props)
+  => SomeComponent props (Component parent props model action)
 -----------------------------------------------------------------------------
 -- | Like '+>' but operates on any 'View', not just 'Component'.
 --
@@ -380,14 +382,14 @@
 --
 -- @since 1.9.0.0
 (+>)
-  :: forall child model action a . Eq child
+  :: forall child childAction model action . Eq child
   => MisoString
   -- ^ 'VComp' 'key_'
-  -> Component model child action
+  -> Component model () child childAction
   -- ^ 'Component'
-  -> View model a
+  -> View model action
 infixr 0 +>
-key +> comp = VComp (Just (toKey key)) (SomeComponent comp)
+key +> comp = VComp (Just (toKey key)) (SomeComponent () comp)
 -----------------------------------------------------------------------------
 -- | t'Miso.Types.Component' mounting combinator.
 --
@@ -396,6 +398,46 @@
 -- the two t'Miso.Types.Component', to ensure unmounting and mounting occurs.
 --
 -- @
+-- mountWithProps someProps $ component model noop $ \\m ->
+--  div_ [ id_ "foo" ] [ text (ms m) ]
+-- @
+--
+-- @since 1.11.0.0
+mountWithProps
+  :: (Eq child, Eq props)
+  => props
+  -- ^ 'props' to use
+  -> Component parent props child action
+  -- ^ 'Component' to mount
+  -> View parent a
+mountWithProps props comp  = VComp Nothing (SomeComponent props comp)
+-----------------------------------------------------------------------------
+-- | t'Miso.Types.Component' mounting combinator.
+--
+-- @
+-- mountWithProps_ "key" someProps $ component model noop $ \\m ->
+--  div_ [ id_ "foo" ] [ text (ms m) ]
+-- @
+--
+-- @since 1.11.0.0
+mountWithProps_
+  :: (Eq child, Eq props)
+  => MisoString
+  -- ^ 'key' to use
+  -> props
+  -- ^ 'props' to use
+  -> Component parent props child action
+  -- ^ 'Component' to mount
+  -> View parent a
+mountWithProps_ key props comp  = VComp (Just (Key key)) (SomeComponent props comp)
+-----------------------------------------------------------------------------
+-- | t'Miso.Types.Component' mounting combinator.
+--
+-- Note: only use this if you're certain you won't be diffing two t'Miso.Types.Component'
+-- against each other. Otherwise, you will need a key to distinguish between
+-- the two t'Miso.Types.Component', to ensure unmounting and mounting occurs.
+--
+-- @
 -- mount_ $ component model noop $ \\m ->
 --  div_ [ id_ "foo" ] [ text (ms m) ]
 -- @
@@ -403,10 +445,10 @@
 -- @since 1.9.0.0
 mount_
   :: Eq child
-  => Component model child a
+  => Component parent () child childAction
   -- ^ 'Component' to mount
-  -> View model action
-mount_ comp = VComp Nothing (SomeComponent comp)
+  -> View parent action
+mount_ comp = VComp Nothing (SomeComponent () comp)
 -----------------------------------------------------------------------------
 -- | DOM element namespace.
 data Namespace
diff --git a/src/Miso/WebSocket.hs b/src/Miso/WebSocket.hs
--- a/src/Miso/WebSocket.hs
+++ b/src/Miso/WebSocket.hs
@@ -61,7 +61,7 @@
   -- ^ @onMessage@ is a callback invoked when a message has been received from the t'WebSocket' server.
   -> (MisoString -> action)
   -- ^ Error message callback
-  -> Effect parent model action
+  -> Effect parent props model action
 connect = websocketConnect
 -----------------------------------------------------------------------------
 -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket>
@@ -80,7 +80,7 @@
   -- ^ @onMessage@ is a callback invoked when a JSON-encoded message has been received from the t'WebSocket' server.
   -> (MisoString -> action)
   -- ^ Error message callback
-  -> Effect parent model action
+  -> Effect parent props model action
 connectJSON = websocketConnectJSON
 -----------------------------------------------------------------------------
 -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket>
@@ -98,7 +98,7 @@
   -- ^ @onMessage@ is a callback invoked when a text-encoded message has been received from the t'WebSocket' server.
   -> (MisoString -> action)
   -- ^ Error message callback
-  -> Effect parent model action
+  -> Effect parent props model action
 connectText = websocketConnectText
 -----------------------------------------------------------------------------
 -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket>
@@ -116,7 +116,7 @@
   -- ^ @onMessage@ is a callback invoked when a binary-encoded message has been received from the t'WebSocket' server.
   -> (MisoString -> action)
   -- ^ @onError@ callback
-  -> Effect parent model action
+  -> Effect parent props model action
 connectBLOB = websocketConnectBLOB
 -----------------------------------------------------------------------------
 -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket>
@@ -134,7 +134,7 @@
   -- ^ @onMessage@ is a callback invoked when an t'ArrayBuffer' message has been received from the t'WebSocket' server.
   -> (MisoString -> action)
   -- ^ @onError@ callback
-  -> Effect parent model action
+  -> Effect parent props model action
 connectArrayBuffer = websocketConnectArrayBuffer
 -----------------------------------------------------------------------------
 -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send>
@@ -147,7 +147,7 @@
 -- instance ToJSON Person where
 --   toJSON (Person name age) = object [ "name" .= name, "age" .= age ]
 --
--- test :: WebSocket -> Effect parent model action
+-- test :: WebSocket -> Effect parent props model action
 -- test connection = do
 --   sendJSON (connection :: WebSocket) (Person "alice" 42)
 --   sendJSON (connection :: WebSocket) (Person "bob" 42)
@@ -160,7 +160,7 @@
   -- ^ t'WebSocket' descriptor required to send a message to the server.
   -> json
   -- ^ A JSON-encoded message
-  -> Effect parent model action
+  -> Effect parent props model action
 sendJSON socket x = websocketSend socket (JSON x)
 -----------------------------------------------------------------------------
 -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send>
@@ -169,7 +169,7 @@
   -- ^ t'WebSocket' descriptor required to send a message to the server.
   -> Blob
   -- ^ An t'Blob' payload to send to the t'WebSocket' server
-  -> Effect parent model action
+  -> Effect parent props model action
 sendBLOB socket x = websocketSend @() socket (blob x)
 -----------------------------------------------------------------------------
 -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send>
@@ -178,7 +178,7 @@
   -- ^ t'WebSocket' descriptor required to send a message to the server.
   -> ArrayBuffer
   -- ^ An t'ArrayBuffer' payload to send to the t'WebSocket' server
-  -> Effect parent model action
+  -> Effect parent props model action
 sendArrayBuffer socket x = websocketSend @() socket (arrayBuffer x)
 -----------------------------------------------------------------------------
 -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send>
@@ -187,7 +187,7 @@
   -- ^ t'WebSocket' descriptor required to send a message to the server.
   -> MisoString
   -- ^ A text payload to send to t'WebSocket' server
-  -> Effect parent model action
+  -> Effect parent props model action
 sendText socket x = websocketSend @() socket (TEXT x)
 -----------------------------------------------------------------------------
 -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close>
@@ -199,6 +199,6 @@
 close
   :: WebSocket
   -- ^ t'WebSocket' descriptor required to close the socket on the server.
-  -> Effect parent model action
+  -> Effect parent props model action
 close = websocketClose
 -----------------------------------------------------------------------------
