diff --git a/ghcjs-src/Miso/Types.hs b/ghcjs-src/Miso/Types.hs
--- a/ghcjs-src/Miso/Types.hs
+++ b/ghcjs-src/Miso/Types.hs
@@ -9,8 +9,17 @@
 ----------------------------------------------------------------------------
 module Miso.Types
   ( App (..)
+
+    -- * The Transition Monad
+  , Transition
+  , fromTransition
+  , toTransition
+  , scheduleIO
   ) where
 
+import           Control.Monad.Trans.Class (lift)
+import           Control.Monad.Trans.State.Strict (StateT(StateT), execStateT)
+import           Control.Monad.Trans.Writer.Strict (WriterT(WriterT), Writer, runWriter, tell)
 import qualified Data.Map           as M
 import           Miso.Effect
 import           Miso.Html.Internal
@@ -21,7 +30,8 @@
   { model :: model
   -- ^ initial model
   , update :: action -> model -> Effect action model
-  -- ^ Function to update model, optionally provide effects
+  -- ^ Function to update model, optionally provide effects.
+  --   See the 'Transition' monad for succinctly expressing model transitions.
   , view :: model -> View action
   -- ^ Function to draw `View`
   , subs :: [ Sub action model ]
@@ -32,3 +42,53 @@
   -- ^ Initial action that is run after the application has loaded
   }
 
+-- | A monad for succinctly expressing model transitions in the 'update' function.
+--
+-- @Transition@ is a state monad so it abstracts over manually passing the model
+-- around. It's also a writer monad where the accumulator is a list of scheduled
+-- IO actions. Multiple actions can be scheduled using
+-- @Control.Monad.Writer.Class.tell@ from the @mtl@ library and a single action
+-- can be scheduled using 'scheduleIO'.
+--
+-- Tip: use the @Transition@ monad in combination with the stateful
+-- <http://hackage.haskell.org/package/lens-4.15.4/docs/Control-Lens-Operators.html lens>
+-- operators (all operators ending in "@=@"). The following example assumes
+-- the lenses @field1@, @counter@ and @field2@ are in scope and that the
+-- @LambdaCase@ language extension is enabled:
+--
+-- @
+-- myApp = App
+--   { update = 'fromTransition' . \\case
+--       MyAction1 -> do
+--         field1 .= value1
+--         counter += 1
+--       MyAction2 -> do
+--         field2 %= f
+--         scheduleIO $ do
+--           putStrLn \"Hello\"
+--           putStrLn \"World!\"
+--   , ...
+--   }
+-- @
+type Transition action model = StateT model (Writer [IO action])
+
+-- | Convert a @Transition@ computation to a function that can be given to 'update'.
+fromTransition
+    :: Transition action model ()
+    -> (model -> Effect action model) -- ^ model 'update' function.
+fromTransition act = uncurry Effect . runWriter . execStateT act
+
+-- | Convert an 'update' function to a @Transition@ computation.
+toTransition
+    :: (model -> Effect action model) -- ^ model 'update' function
+    -> Transition action model ()
+toTransition f = StateT $ \s ->
+                   let Effect s' ios = f s
+                   in WriterT $ pure (((), s'), ios)
+
+-- | Schedule a single IO action for later execution.
+--
+-- Note that multiple IO action can be scheduled using
+-- @Control.Monad.Writer.Class.tell@ from the @mtl@ library.
+scheduleIO :: IO action -> Transition action model ()
+scheduleIO ioAction = lift $ tell [ ioAction ]
diff --git a/miso.cabal b/miso.cabal
--- a/miso.cabal
+++ b/miso.cabal
@@ -1,5 +1,5 @@
 name:                miso
-version:             0.7.8.0
+version:             0.8.0.0
 category:            Web, Miso, Data Structures
 license:             BSD3
 license-file:        LICENSE
@@ -277,7 +277,8 @@
     bytestring,
     containers,
     servant,
-    text
+    text,
+    transformers
   if impl(ghcjs)
     hs-source-dirs:
       ghcjs-src
