diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for react
+
+## 0.1.0.0 -- 2023-07-03
+
+* Support for writing basic React widgets in Haskell
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2023, Obsidian Systems LLC
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Obsidian Systems LLC nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/react.cabal b/react.cabal
new file mode 100644
--- /dev/null
+++ b/react.cabal
@@ -0,0 +1,56 @@
+cabal-version:      3.0
+name:               react
+version:            0.1.0.0
+license:            BSD-3-Clause
+license-file:       LICENSE
+author:             Obsidian Systems LLC
+maintainer:         maintainer@obsidian.systems
+category:           Web
+build-type:         Simple
+extra-doc-files:    CHANGELOG.md
+synopsis:           Create React components in Haskell
+description:
+  This library provides bindings to React that allow you to write components in
+  Haskell.  Typically, these components will be compiled with GHCJS to produce
+  JavaScript for inclusion in a React application.
+
+common warnings
+    ghc-options: -Wall -Wunused-packages
+
+library
+  import:           warnings
+  exposed-modules:
+    React
+    React.Component
+    React.Element
+    React.Export
+    React.Hook
+    React.JSaddle
+    React.Misc
+    React.Types
+
+  build-depends:
+    base                               >= 4.14.3 && < 4.15,
+    containers                         >= 0.6.5 && < 0.7,
+    jsaddle                            >= 0.9.8 && < 0.10,
+    template-haskell                   >= 2.16.0 && < 2.17,
+    text                               >= 1.2.4 && < 1.3,
+    mtl                                >= 2.2.2 && < 2.3
+  hs-source-dirs:
+    src
+
+  if impl(ghcjs)
+    build-depends: ghcjs-base == 0.2.*
+                  , ghcjs-prim
+                  -- This is to allow the hashable patches to work
+                  -- the hashable should to be pinned in reflex-platform
+                  , hashable == 1.3.5.0
+  else
+    build-depends: jsaddle-warp >= 0.9.8 && < 0.10
+
+  default-language: Haskell2010
+  default-extensions:
+    FlexibleInstances
+    GeneralizedNewtypeDeriving
+    LambdaCase
+    OverloadedStrings
diff --git a/src/React.hs b/src/React.hs
new file mode 100644
--- /dev/null
+++ b/src/React.hs
@@ -0,0 +1,9 @@
+module React (module X) where
+
+import React.Component as X
+import React.Element as X
+import React.Export as X
+import React.Hook as X
+import React.JSaddle as X
+import React.Misc as X
+import React.Types as X
diff --git a/src/React/Component.hs b/src/React/Component.hs
new file mode 100644
--- /dev/null
+++ b/src/React/Component.hs
@@ -0,0 +1,27 @@
+module React.Component where
+
+import Control.Monad.Except
+import Control.Monad.Reader
+import Language.Javascript.JSaddle hiding (Ref)
+
+import React.JSaddle
+import React.Types
+
+--TODO: The Hook section shouldn't have any control flow to it; probably it also shouldn't depend on props except in specific ways
+component
+  :: FromJSVal props
+  => (props -> Hook Element)
+  -> ReaderT React JSM (Component props ())
+component hook = do
+  react <- ask
+  f <- lift $ function' $ \_ _ args -> flip runReaderT react $ do
+    let propsVal = case args of
+          [] -> jsUndefined
+          arg0 : _ -> arg0
+    props <- liftJSM $ fromJSVal propsVal >>= \case
+      Nothing -> fail "Invalid props"
+      Just props -> pure props
+    e <- unHook $ hook props
+    unElement e
+  pure $ Component f
+
diff --git a/src/React/Element.hs b/src/React/Element.hs
new file mode 100644
--- /dev/null
+++ b/src/React/Element.hs
@@ -0,0 +1,25 @@
+module React.Element where
+
+import Control.Monad.Except
+import Control.Monad.Reader
+import Data.Map (Map)
+import Data.Text (Text)
+import Language.Javascript.JSaddle hiding (Ref)
+
+import React.Misc
+import React.Types
+
+createElement :: Tag -> Map Text JSVal -> [Element] -> Element
+createElement etag props children = Element $ do
+  react <- ask
+  createdChildren <- mapM unElement children
+  lift $ react # t "createElement" $ [pure $ unTag etag, toJSVal props] <> fmap pure createdChildren
+
+createFragment :: [Element] -> Element
+createFragment = createFragmentWithProps mempty
+
+createFragmentWithProps :: Map Text JSVal -> [Element] -> Element
+createFragmentWithProps props children = Element $ do
+  react <- ask
+  fragmentTag <- lift $ fmap Tag $ react ! t "Fragment"
+  unElement $ createElement fragmentTag props children
diff --git a/src/React/Export.hs b/src/React/Export.hs
new file mode 100644
--- /dev/null
+++ b/src/React/Export.hs
@@ -0,0 +1,63 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE TemplateHaskell #-}
+module React.Export where
+
+import Control.Monad.Reader as MTL
+import Data.Map (Map)
+import qualified Data.Map as Map
+import Data.Text (Text)
+import qualified Data.Text as T
+import Language.Haskell.TH
+import Language.Haskell.TH.Syntax as TH
+import Language.Javascript.JSaddle
+
+import React.Misc
+import React.JSaddle ()
+import React.Types
+
+#ifndef ghcjs_HOST_OS
+import Control.Monad.Except
+import qualified Data.Text.IO as T
+import Language.Javascript.JSaddle.Warp
+#endif
+
+mainExportsToJS :: [Name] -> Q [Dec]
+mainExportsToJS names = [d|
+    main :: IO ()
+    main = exportToJSIO $ sequence $ Map.fromList $(listE $ fmap nameToExportEntry names)
+  |]
+
+nameToExportEntry :: Name -> Q Exp
+nameToExportEntry n = [| (T.pack $(TH.lift $ nameBase n), MTL.lift . toJSVal =<< $(varE n)) |]
+
+exportToJSIO :: ReaderT React JSM (Map Text JSVal) -> IO ()
+exportToJSIO build = runJS $ \arg -> do
+  react <- fmap (React . Object) $ arg ! t "react"
+  m <- flip runReaderT react build
+  _ <- (arg # t "setVal") [m]
+  pure ()
+
+runJS :: (JSVal -> JSM ()) -> IO ()
+
+#ifdef ghcjs_HOST_OS
+
+foreign import javascript unsafe "getProgramArg"
+    getProgramArg :: JSM JSVal
+
+runJS f = do
+  arg <- getProgramArg
+  f arg
+
+#else
+
+runJS f = do
+  let port = 3001 --TODO: Get this from npm config or something
+  run port $ \arg -> f arg `catchError` printJavaScriptException
+
+printJavaScriptException :: JavaScriptException -> JSM ()
+printJavaScriptException (JavaScriptException e) = do
+  s <- e # t "toString" $ ()
+  j <- valToJSON s
+  liftIO $ T.putStrLn $ "Exception: " <> tshow j
+
+#endif
diff --git a/src/React/Hook.hs b/src/React/Hook.hs
new file mode 100644
--- /dev/null
+++ b/src/React/Hook.hs
@@ -0,0 +1,68 @@
+module React.Hook where
+
+import Prelude hiding ((!!))
+
+import Control.Monad
+import Control.Monad.Except
+import Control.Monad.Reader
+import Language.Javascript.JSaddle hiding (Ref)
+
+import React.JSaddle
+import React.Misc
+import React.Types
+
+--TODO: Input can be an initializer function rather than value
+--TODO: `set` can take `a -> a` instead of `a`
+--TODO: I bet React always returns the same function object for the setter; if we re-wrap the function using `useCallback` each time, we are probably hurting performance by making it be a new object each time and forcing rerendering of children
+useState :: (ToJSVal a, FromJSVal a) => a -> Hook (a, a -> JSM ())
+useState initialValue = Hook $ do
+  react <- ask
+  initialJSVal <- lift $ toJSVal initialValue
+  result <- lift $ (react # t "useState") initialJSVal
+  Just s <- lift $ fromJSVal =<< result !! 0 --TODO: Exception handling
+  setter <- lift $ result !! 1
+  pure
+    ( s
+    , \v' -> void $ call setter nullObject [v']
+    )
+
+useRef :: JSVal -> Hook JSVal
+useRef initialValue = Hook $ do
+  react <- ask
+  lift $ (react # t "useRef") initialValue
+
+useEffect :: (JSVal -> JSVal -> [JSVal] -> JSM JSVal) -> Maybe [JSVal] -> Hook ()
+useEffect f deps = Hook $ do
+  react <- ask
+  Function' _ cb <- lift $ function' f
+  depsArg <- case deps of
+    Nothing -> pure []
+    Just someDeps -> do
+      depsArray <- lift $ toJSVal someDeps
+      pure [depsArray]
+  _ <- lift $ (react # t "useEffect") $ [pToJSVal cb] <> depsArg
+  pure ()
+
+useMemo :: (ToJSVal a, FromJSVal a) => JSM a -> Maybe [JSVal] -> Hook a
+useMemo a deps = Hook $ do
+  react <- ask
+  Function' _ cb <- lift $ function' $ \_ _ _ -> toJSVal =<< a
+  depsArg <- case deps of
+    Nothing -> pure []
+    Just someDeps -> do
+      depsArray <- lift $ toJSVal someDeps
+      pure [depsArray]
+  resultVal <- lift $ (react # t "useMemo") $ [pToJSVal cb] <> depsArg
+  Just result <- lift $ fromJSVal resultVal
+  pure result
+
+useCallback :: ToJSVal result => (JSVal -> JSVal -> [JSVal] -> JSM result) -> Maybe [JSM JSVal] -> Hook JSVal
+useCallback f deps = Hook $ do
+  react <- ask
+  Function' _ cb <- lift $ function' $ \fObj this args -> toJSVal =<< f fObj this args
+  depsArg <- case deps of
+    Nothing -> pure []
+    Just someDeps -> do
+      depsArray <- lift $ toJSVal =<< sequence someDeps
+      pure [depsArray]
+  lift $ (react # t "useCallback") $ [pToJSVal cb] <> depsArg
diff --git a/src/React/JSaddle.hs b/src/React/JSaddle.hs
new file mode 100644
--- /dev/null
+++ b/src/React/JSaddle.hs
@@ -0,0 +1,89 @@
+{-# LANGUAGE CPP #-}
+{-# OPTIONS_GHC -Wno-orphans #-}
+-- | Everything in this module belongs in JSaddle, GHCJS-DOM, or similar
+module React.JSaddle where
+
+import Prelude hiding ((!!))
+
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Map (Map)
+import qualified Data.Map as Map
+import Control.Monad
+import Data.String
+
+import React.Misc
+
+#ifdef ghcjs_HOST_OS
+import Data.Coerce (coerce)
+import GHCJS.Foreign.Callback
+import qualified JavaScript.Array as Array (toListIO)
+import Language.Javascript.JSaddle
+#else
+import GHCJS.Prim.Internal (primToJSVal)
+import Language.Javascript.JSaddle hiding (Ref)
+#endif
+
+#ifndef ghcjs_HOST_OS
+instance PToJSVal Text where
+  pToJSVal s = primToJSVal $ PrimVal_String s
+
+instance PToJSVal Int where
+  pToJSVal i = primToJSVal $ PrimVal_Number $ fromIntegral i
+#endif
+
+instance PToJSVal Function where
+  pToJSVal (Function _ o) = pToJSVal o
+
+instance PToJSVal Object where
+  pToJSVal (Object v) = v
+
+instance IsString JSVal where
+  fromString = pToJSVal . T.pack
+
+instance ToJSVal v => ToJSVal (Map Text v) where
+  toJSVal m = do
+    o@(Object oVal) <- obj
+    forM_ (Map.toList m) $ \(k, v) -> do
+      (o <# k) =<< toJSVal v
+    pure oVal
+
+consoleLog :: ToJSVal a => a -> JSM JSVal
+consoleLog x = (global ! t "console") # t "log" $ [x]
+
+type JSCallAsFunction' = JSVal      -- ^ Function object
+                     -> JSVal      -- ^ this
+                     -> [JSVal]    -- ^ Function arguments
+                     -> JSM JSVal  -- ^ Return value
+
+function' :: JSCallAsFunction' -- ^ Haskell function to call
+         -> JSM Function'     -- ^ Returns a JavaScript function object that will
+                             --   call the Haskell one when it is called
+#ifdef ghcjs_HOST_OS
+function' f = do
+    callback <- syncCallback2' $ \this args -> do
+        rargs <- Array.toListIO (coerce args)
+        f this this rargs -- TODO pass function object through
+    Function' callback <$> makeFunctionWithCallback' callback
+#else
+function' f = do
+    (cb, f') <- newSyncCallback'' f --TODO: "ContinueAsync" behavior
+    return $ Function' cb $ Object f'
+#endif
+
+#ifdef ghcjs_HOST_OS
+data Function' = Function' {functionCallback' :: Callback (JSVal -> JSVal -> IO JSVal), functionObject' :: Object}
+#else
+data Function' = Function' {functionCallback' :: CallbackId, functionObject' :: Object}
+#endif
+
+#ifdef ghcjs_HOST_OS
+foreign import javascript unsafe "$r = function () { return $1(this, arguments); }"
+    makeFunctionWithCallback' :: Callback (JSVal -> JSVal -> IO JSVal) -> IO Object
+#endif
+
+instance ToJSVal Function' where
+    toJSVal = toJSVal . functionObject'
+
+instance PToJSVal Function' where
+  pToJSVal (Function' _ o) = pToJSVal o
diff --git a/src/React/Misc.hs b/src/React/Misc.hs
new file mode 100644
--- /dev/null
+++ b/src/React/Misc.hs
@@ -0,0 +1,11 @@
+module React.Misc where
+
+import Data.Text (Text)
+import qualified Data.Text as T
+
+t :: Text -> Text
+t = id
+
+tshow :: Show a => a -> Text
+tshow = T.pack . show
+
diff --git a/src/React/Types.hs b/src/React/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/React/Types.hs
@@ -0,0 +1,41 @@
+{-# LANGUAGE CPP #-}
+module React.Types where
+
+import Control.Monad.Reader
+import Data.String
+import qualified Data.Text as T
+import Language.Javascript.JSaddle hiding (Ref)
+
+import React.JSaddle
+
+-- | An object that contains the React library
+newtype React = React { unReact :: Object }
+
+instance MakeObject React where
+  makeObject = pure . unReact
+
+newtype Component props refVal = Component { unComponent :: Function' }
+  deriving (ToJSVal, PToJSVal)
+
+instance MakeObject (Component props refVal) where
+  makeObject = makeObject . functionObject' . unComponent
+
+newtype Hook a = Hook { unHook :: ReaderT React JSM a }
+  deriving ( Functor
+           , Applicative
+           , Monad
+           , MonadJSM
+#ifndef ghcjs_HOST_OS
+           , MonadIO
+#endif
+           )
+
+newtype Element = Element { unElement :: ReaderT React JSM JSVal }
+
+instance IsString Element where
+  fromString = Element . pure . pToJSVal . T.pack
+
+newtype Tag = Tag { unTag :: JSVal }
+
+instance IsString Tag where
+  fromString = Tag . pToJSVal . T.pack
