diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,14 @@
 
 ## Unreleased
 
-## 0.3.0
+## 0.3.1 - 2024-02-16
+ - Added `Reactor` type and `Reaction` typeclass for heterogeneous components
+ - Updated Usage documentation
+
+## 0.3.0 META - 2024-01-09
+ - Changed dependency bounds to make the build on Hackage work.
+
+## 0.3.0 - 2023-12-15
  - Made `Sized`'s `sized` function's size a single argument
  - Made the `Sized` container properly override the inherent size of content.
  - Made Window size a single argument
diff --git a/FULE.cabal b/FULE.cabal
--- a/FULE.cabal
+++ b/FULE.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           FULE
-version:        0.3.0
+version:        0.3.1
 synopsis:       Functional UI Layout Engine
 description:    Please see the README on GitHub at <https://github.com/pschnapp/FULE#readme>
 category:       GUI
@@ -51,17 +51,18 @@
       FULE.Internal.Util
       FULE.Layout
       FULE.LayoutOp
+      FULE.Reactor
   other-modules:
       Paths_FULE
   hs-source-dirs:
       src
-  ghc-options: -Wredundant-constraints
+  ghc-options: -Wredundant-constraints -Wunused-packages
   build-depends:
       base >=4.7 && <5
-    , containers >=0.6.6 && <0.7
-    , deepseq >=1.4.8 && <1.5
-    , mtl >=2.2.2 && <2.3
-    , transformers >=0.5.6 && <0.6
+    , containers >=0.6.5 && <0.7
+    , deepseq >=1.4.6 && <1.5
+    , mtl >=2.2.2 && <2.4
+    , transformers >=0.5.6 && <0.7
   default-language: Haskell2010
 
 test-suite FULE-test
@@ -71,6 +72,7 @@
       Divided
       Grid
       Layout
+      Reactor
       Paths_FULE
   hs-source-dirs:
       examples
@@ -79,8 +81,8 @@
   build-depends:
       FULE
     , base >=4.7 && <5
-    , containers >=0.6.6 && <0.7
-    , deepseq >=1.4.8 && <1.5
-    , mtl >=2.2.2 && <2.3
-    , transformers >=0.5.6 && <0.6
+    , containers >=0.6.5 && <0.7
+    , deepseq >=1.4.6 && <1.5
+    , mtl >=2.2.2 && <2.4
+    , transformers >=0.5.6 && <0.7
   default-language: Haskell2010
diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright Paul Schnapp (c) 2022, 2023
+Copyright Paul Schnapp (c) 2022-2024
 
 All rights reserved.
 
@@ -13,7 +13,7 @@
       disclaimer in the documentation and/or other materials provided
       with the distribution.
 
-    * Neither the name of Author name here nor the names of other
+    * Neither the name of the author nor the names of other
       contributors may be used to endorse or promote products derived
       from this software without specific prior written permission.
 
diff --git a/doc/Usage.md b/doc/Usage.md
--- a/doc/Usage.md
+++ b/doc/Usage.md
@@ -2,7 +2,7 @@
 
 FULE is a layout engine for calculating the positions of visual elements on screen and adjusting these positions in response to input. While it does not draw the the elements or facilitate interactions with them -- these are left up to you -- it does try to give you a leg up in your effort to do so!
 
-Creating a layout with FULE can be done at a high level utilizing its predefined containers, at a low level with the `LayoutDesign` and `Layout` types using guides, or at a mix of both levels. There are some simple examples in the [examples](../examples/) directory if you'd like to view some working code.
+Creating a layout with FULE can be done at a high level utilizing its predefined containers, at a low level with the `LayoutDesign` type using guides, or at a mix of both levels. There are some simple examples in the [examples](../examples/) directory if you'd like to view some working code.
 
 This document goes over FULE's essential concepts and discusses how to integrate FULE into your project.
 
@@ -19,9 +19,9 @@
 
 Guides can be positioned absolutely, statically relative to other guides, or suspended elastically between other guides.
 
-At a higher level are _containers_. Containers use guides under the hood to create higher-level constructs such as grids or divided areas. When you create a `Layout` you will probably use continers.
+At a higher level are _containers_. Containers use guides under the hood to create higher-level constructs such as grids or divided areas. When you create a `Layout` you will probably use containers.
 
-That's pretty-much it as far as usage concepts go; you can dive into [the theory](Theory.md) if you'd like to look a little deeper.
+That's pretty-much it as far as usage concepts go. You can dive into [the theory](Theory.md) if you'd like to look a little deeper; otherwise, onward!
 
 # Imports
 
@@ -39,9 +39,9 @@
 
 `Component` is a monad-friendly typeclass should you need to interact with stateful data to retrieve sizing information for your components.
 
-Your components will either need to be all of the same type or to have some sort of [wrapper for heterogenous types](https://wiki.haskell.org/Heterogenous_collections) around them in order for FULE to use them.
+Your components will either need to be all of the same type or to have some sort of [wrapper for heterogeneous types](https://wiki.haskell.org/Heterogenous_collections) around them in order for FULE to use them. As of **v0.3.1** FULE offers the `Reactor` and `Reactor'` types for this; types contained in them are interacted with by implementing the `Reaction` typeclass.
 
-A convenience implementation of `Component` has been provided for you for any type which specifices no height or width requirements. When you define your own instance(s) you should override the default by using the `{-# OVERLAPS #-}` or `{-# OVERLAPPING #-}` pragmas on it (or them).
+A convenience implementation of `Component` has been provided for you, for any type, which specifices no height or width requirements. When you define your own instance(s) you should override the default by using the `{-# OVERLAPS #-}` or `{-# OVERLAPPING #-}` pragmas on it (or them).
 
 ## Minimal Layout
 
@@ -49,11 +49,11 @@
 
 Layouts are created at a high level using any of several _containers_. The outer-most container for your layout must be a `Window`; within the `Window` you can add your visual components and other containers. 
 
-Most containers require configuration arguments suitable for their particular functions; `Window` must be provided:
+Most containers require configuration arguments suitable for their particular functions; `Window` must be provided with:
  - The dimensions of the GUI window that the layout will be displayed in
- - A function to create an (invisible) UI component that you should use to adjust the size of the `Layout` in response to changes in the GUI window's size -- a callback in other words
+ - A function to create an (invisible) UI component that you should use to adjust the size of the `Layout` in response to changes in the GUI window's size -- a callback, in other words
 
-For the requirements other containers have, consult their documentation on Hackage (not available yet). For some working examples, see the [examples](../examples/) directory.
+For the requirements other containers have, consult [their documentation on Hackage](https://hackage.haskell.org/package/FULE). For some working examples, see the [examples](../examples/) directory.
 
 Once your layout has been defined, you'll build it by passing the `Window` to one of the `layout` or `layoutM` functions[^1]. The result of this will be a tuple of type `(Layout, [ComponentInfo k])`, where `k` is your component type.
 
@@ -101,7 +101,7 @@
     Just cl -> toRectangle bounds layout : toRectangles cl layout
 ```
 
-The rectangle at the head of the returned list is the bounds for the component; all the rectangles in the list should be intersected to find the clipped display area. (Don't forget to consider clipping when reacting to input!)
+The rectangle at the head of the list returned by this function is the bounds for the component; all the rectangles in the list should be intersected to find the clipped display area. (Don't forget to consider clipping when reacting to input!)
 
 # Conclusion
 
diff --git a/examples/Reactor.hs b/examples/Reactor.hs
new file mode 100644
--- /dev/null
+++ b/examples/Reactor.hs
@@ -0,0 +1,256 @@
+-- This module contains a full example of a Presenter in a Model-View-Presenter
+-- architecture utilizing `Reactor`s.
+--
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Reactor where
+
+import Control.Monad
+import Data.Maybe
+import FULE
+
+
+--------------------------------
+-- VIEW
+--
+-- This section contains types and functions which represent the interaction
+-- with the View of the MVP.
+--------------------------------
+
+data UIMessage
+  = WindowSizeChanged
+    { windowWidthOf :: Int
+    , windowHeightOf :: Int
+    }
+  | MouseMoved
+    { xPositionOf :: Int
+    , yPositionOf :: Int
+    , xDeltaOf :: Int
+    , yDeltaOf :: Int
+    }
+
+data ForView = ForView
+
+instance Semigroup ForView where
+  a <> b = ForView
+
+instance Monoid ForView where
+  mempty = ForView
+
+getUIMessage :: (Monad m) => m UIMessage
+getUIMessage = undefined -- for you to implement
+
+interactWithView :: (Monad m) => ForView -> m UIMessage
+interactWithView = undefined -- for you to implement
+
+
+--------------------------------
+-- MODEL
+--
+-- This section contains types and functions which represent the interaction
+-- with the Model of the MVP.
+--------------------------------
+
+data FromModel
+
+data ForModel = ForModel
+
+instance Semigroup ForModel where
+  a <> b = ForModel
+
+instance Monoid ForModel where
+  mempty = ForModel
+
+interactWithModel :: (Monad m) => ForModel -> m FromModel
+interactWithModel = undefined -- for you to implement
+
+
+--------------------------------
+-- Reactor
+--
+-- This section contains types used to interact with a Reactor's contents
+-- via the Reaction typeclass.
+--
+-- These types are generic so that the same set of data constructors could be
+-- used with multiple MVP triads.
+--------------------------------
+
+data ReactorInput mi
+  = LayoutInput Layout Bounds -- bounds for a component
+  | ModelInput mi
+  | NoInput
+  | ViewInput UIMessage
+
+-- GADT for selecting the `getProduct`'s output type (the `o` param)
+data ReactorOutput mo po vo o where
+  LayoutOutput :: ReactorOutput mo po vo (Layout -> Layout)
+  ModelOutput :: ReactorOutput mo po vo mo
+  NoOutput :: ReactorOutput mo po vo ()
+  PresenterOutput :: ReactorOutput mo po vo po
+  ViewOutput :: ReactorOutput mo po vo vo
+
+
+--------------------------------
+-- Window Control
+--------------------------------
+
+data WindowWidget
+  = ResizeControl
+    { currentWidthOf :: Int
+    , currentHeightOf :: Int
+    , deltaWidthOf :: Int
+    , deltaHeightOf :: Int
+    , widthGuideOf :: GuideID
+    , heightGuideOf :: GuideID
+    }
+
+instance Reaction WindowWidget (ReactorInput mi) (ReactorOutput mo po vo) where
+  addReactant (ViewInput (WindowSizeChanged w h)) _ ww =
+    -- record the delta at a window size change
+    ww
+    { currentWidthOf = w
+    , currentHeightOf = h
+    , deltaWidthOf = w - currentWidthOf ww
+    , deltaHeightOf = h - currentHeightOf ww
+    }
+  addReactant _ _ ww = -- catchall for other input cases
+    -- reset the deltas
+    ww { deltaWidthOf = 0, deltaHeightOf = 0 }
+  getProduct _ LayoutOutput ww =
+    -- react to a window size change if there has been one
+    if dw == 0 && dh == 0 then Nothing else Just . reactToChanges
+    . (if dw /= 0 then ((wg, dw):) else id)
+    . (if dh /= 0 then ((hg, dh):) else id)
+    $ []
+    where
+      ResizeControl
+        { deltaWidthOf = dw
+        , deltaHeightOf = dh
+        , widthGuideOf = wg
+        , heightGuideOf = hg
+        } = ww
+  getProduct _ _ _ = Nothing -- catchall for other output cases
+
+makeWindowWidget
+  :: (Monad m)
+  => Int -> Int -> GuideID -> GuideID
+  -> Reactor (ReactorInput mi) (ReactorOutput mo po vo) m
+makeWindowWidget w h wg hg = reactor (ResizeControl w h 0 0 wg hg)
+
+
+--------------------------------
+-- UI Widgets
+--------------------------------
+
+data UIWidget = UIWidget
+
+instance Reaction UIWidget (ReactorInput mi) (ReactorOutput mo po vo) where
+  addReactant _ _ uw = uw
+  getProduct _ _ _ = Nothing
+
+makeUIWidget
+  :: (Monad m)
+  => Reactor (ReactorInput mi) (ReactorOutput mo po vo) m
+makeUIWidget = reactor UIWidget
+
+
+--------------------------------
+-- Presenter
+--------------------------------
+
+data ForPresenter
+  = NotQuit
+  | Quit
+  deriving (Eq)
+
+hasQuit :: [ForPresenter] -> Bool
+hasQuit = elem Quit
+
+
+-- some convenience aliases
+type RIN = ReactorInput FromModel
+type ROUT = ReactorOutput ForModel ForPresenter ForView
+type R = Reactor RIN ROUT
+type I m = ItemM m (R m)
+type CIRs m = [ComponentInfo (R m)]
+
+
+getWindowSize :: (Monad m) => m (Int, Int)
+getWindowSize = undefined -- for you to implement
+
+-- When using `ItemM` you'll need to specify a `forall` for your monadic
+-- type-variable in the type-signature and use the `ScopedTypeVariables`
+-- language-extension to get the type-checker to use the same variable for the
+-- type-signature and the type-annotation of your `ItemM` list in the body.
+genLayout :: forall m . (Monad m) => m (Layout, [ComponentInfo (R m)])
+genLayout = do
+  (width, height) <- getWindowSize
+  layoutM
+    (window (width, height) (makeWindowWidget width height)
+      (grid (2, 3)
+        -- Annoyingly type-annotations are required for the list of items which
+        -- must be contained within an extra set of parentheses.
+        ([item (makeUIWidget :: R m)
+        , item (makeUIWidget :: R m)
+        , item (makeUIWidget :: R m)
+        , item (makeUIWidget :: R m)
+        , item (makeUIWidget :: R m)
+        , item (makeUIWidget :: R m)
+        ]::[I m])))
+
+
+-- for whatever reason the compiler has trouble resolving the types of the
+-- Reaction methods so we have to do it manually:
+
+addReact :: (Monad m) => RIN -> ROUT o -> R m -> R m
+addReact = addReactant
+
+getProd :: (Monad m) => RIN -> ROUT o -> R m -> Maybe o
+getProd = getProduct
+
+
+addInput :: (Monad m) => RIN -> CIRs m -> CIRs m
+addInput input = map (addReact input NoOutput <$>)
+
+getOutputs :: (Monad m) => ROUT o -> CIRs m -> [o]
+getOutputs outputType = mapMaybe (getProd NoInput outputType . componentOf)
+
+adjustComponentLayouts
+  :: (Monad m) => Layout -> CIRs m -> CIRs m
+adjustComponentLayouts layout =
+  map (\i -> addReact (LayoutInput layout (boundsOf i)) NoOutput <$> i)
+
+
+presenterUI :: (Monad m) => m ()
+presenterUI = do
+  (layout, infos) <- genLayout
+  viewIn <- getUIMessage
+  -- prime each component with layout info
+  let infos' = adjustComponentLayouts layout infos
+  presenterUILoop layout infos' viewIn
+
+presenterUILoop :: (Monad m) => Layout -> CIRs m -> UIMessage -> m ()
+presenterUILoop layout infos0 viewIn = do
+  -- VIEW (contd)
+  let infos1 = addInput (ViewInput viewIn) infos0
+  -- PRESENTER
+  let presenterOut = getOutputs PresenterOutput infos1
+  unless (hasQuit presenterOut) $ do
+    -- MODEL
+    let modelOut = mconcat $ getOutputs ModelOutput infos1
+    modelIn <- interactWithModel modelOut
+    let infos2 = addInput (ModelInput modelIn) infos1
+    -- LAYOUT
+    let adjustLayout = foldl (.) id $ getOutputs LayoutOutput infos2
+    let layout' = adjustLayout layout
+    let infos3 = adjustComponentLayouts layout' infos2
+    -- VIEW (begin)
+    let viewOut = mconcat $ getOutputs ViewOutput infos3
+    viewIn' <- interactWithView viewOut
+    -- LOOP
+    presenterUILoop layout' infos3 viewIn'
+    -- PSA: DON'T BIND LIKE THIS FOR THE RECURSION AS IT'S NOT TAIL-RECURSIVE:
+    --presenterUILoop layout' infos3 =<< interactWithView viewOut
+
diff --git a/src/FULE.hs b/src/FULE.hs
--- a/src/FULE.hs
+++ b/src/FULE.hs
@@ -23,6 +23,7 @@
  , module FULE.Container.Window
  , module FULE.Layout
  , module FULE.LayoutOp
+ , module FULE.Reactor
  ) where
 
 import FULE.Component
@@ -41,4 +42,5 @@
 import FULE.Container.Window
 import FULE.Layout
 import FULE.LayoutOp
+import FULE.Reactor
 
diff --git a/src/FULE/Reactor.hs b/src/FULE/Reactor.hs
new file mode 100644
--- /dev/null
+++ b/src/FULE/Reactor.hs
@@ -0,0 +1,104 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+
+-- |
+-- Module      : FULE.Reactor
+-- Description : The @Reactor@ and @Reactor'@ types and the @Reaction@ typeclass.
+-- Copyright   : (c) Paul Schnapp, 2024
+-- License     : BSD3
+-- Maintainer  : Paul Schnapp <paul.schnapp@gmail.com>
+--
+-- The terminology used in this module is based on that of a chemical reaction:
+--
+--  - A chemical reaction is given /reactants/ and produces /products/.
+--  - Reactions are contained in /reactors/.
+--
+-- The @Reactor@ and (strict) @Reactor'@ types act as containers for
+-- heterogeneous 'FULE.Component.Component' types.
+--
+-- The @Reaction@ typeclass establishes an interface for interacting with @Reactor@s'
+-- contents (beyond what the @Component@ typeclass allows), providing for adding
+-- in reactants (inputs) and getting out products (outputs).
+module FULE.Reactor
+ ( Reaction(..)
+ , Reactor
+ , reactor
+ , Reactor'
+ , reactor'
+ ) where
+
+import Control.DeepSeq
+
+import FULE.Component
+
+
+-- References:
+-- https://wiki.haskell.org/Heterogenous_collections
+
+
+-- | An interface for interacting with the contents of a @Reactor@:
+--
+--    - The @r@ type-parameter is the reacting type (the data inside the @Reactor@).
+--    - The @i@ type-parameter is the type of input the reaction will accept
+--      in the @addReactant@ method.
+--    - The @ogadt@ type-parameter is a higher-order type used to specify what type
+--      of output the @getProduct@ method should produce. You'll likely want to
+--      use a GADT for this type.
+--
+--   __IMPORTANT NOTE:__ when implementing these methods you'll need a catchall
+--   case for any input or output cases you're not handling, otherwise you'll
+--   get a runtime error!
+class Reaction r i ogadt where
+
+  -- | Add a reactant (input) to the reaction.
+  addReactant :: i -> ogadt o -> r -> r
+  addReactant _ _ = id
+
+  -- | Get a product (output) from the reaction.
+  getProduct :: i -> ogadt o -> r -> Maybe o
+  getProduct _ _ _ = Nothing
+
+
+-- | A container for heterogeneous types. The contents can be interacted with
+--   using the 'Reaction' typeclass.
+data Reactor i ogadt m =
+  forall r . (Component r m, Reaction r i ogadt) => Reactor r
+
+instance (Monad m) => Component (Reactor i ogadt m) m where
+  requiredWidth (Reactor r) = requiredWidth r
+  requiredHeight (Reactor r) = requiredHeight r
+
+instance Reaction (Reactor i ogadt m) i ogadt where
+  addReactant i ogadt (Reactor r) = Reactor (addReactant i ogadt r)
+  getProduct i ogadt (Reactor r) = getProduct i ogadt r
+
+-- | Construct a @Reactor@.
+reactor :: (Component r m, Reaction r i ogadt) => r -> Reactor i ogadt m
+reactor = Reactor
+
+
+-- | A container for heterogeneous types which is strict in its contents.
+--   The contents can be interacted with using the 'Reaction' typeclass.
+--
+--   In addition to implementing the @Component@ and @Reaction@ typeclasses like
+--   the 'Reactor' type, this type implements the 'Control.DeepSeq.NFData'
+--   typeclass from @Control.DeepSeq@ so evaluation of its contents can be forced.
+data Reactor' i ogadt m =
+  forall r . (Component r m, NFData r, Reaction r i ogadt) => Reactor' !r
+
+instance (Monad m) => Component (Reactor' i ogadt m) m where
+  requiredWidth (Reactor' r) = requiredWidth r
+  requiredHeight (Reactor' r) = requiredHeight r
+
+instance NFData (Reactor' i ogadt m) where
+  rnf (Reactor' r) = rnf r
+
+instance Reaction (Reactor' i ogadt m) i ogadt where
+  addReactant i ogadt (Reactor' r) = Reactor' (addReactant i ogadt r)
+  getProduct i ogadt (Reactor' r) = getProduct i ogadt r
+
+-- | Construct a @Reactor'@.
+reactor' :: (Component r m, NFData r, Reaction r i ogadt) => r -> Reactor' i ogadt m
+reactor' = Reactor'
+
