Glazier.React
contains efficient haskell bindings to React JS where render will only be called for the react components with changed states.
It uses the haskell glazier library to enable composable windows
Using React.PureComponent
and react render
will only be called for component who's state actually changed, instead of requiring react to diff the entire DOM.
Glazier
allows disciplined and lawful ways of creating composable widgets. Larger can be created out of other widgets without modifying existing widget code, or manual "lifting state" into larger widgets.
For example, List Widget creates a list of any other widget.
The stateful effects are pure and do not involve IO. This has the benefit of allowing better testing of the intention of gadgets; increasing confidence of the behaviour of the gadget, reducing the surface area of IO misbehaviour.
There are only two places where IO is allowed:
- in the gadget
Command
interpreter, - in the event callback handlers, due to the need to read properties from
javascript
and dispatchingActions
. Besides dispatchingActions
, it is bad practice to create any other observable side effects in event handlers.
AFAIK, Haskell is the only language where you can combine multiple concurrent stateful effects consistently.
Glazier.React
uses ghcjs-base-stub allows compiling GHCJS projects using GHC, which means you can develop using intero.
Glazier.React
uses disposable to ease cleanup of GHCJS callbacks. It also uses a Free Monad Maker
DSL to ease creation of callbacks for widgets.
React elements can be coded using blaze/lucid-style do
notation using ReactMlT
All state and processing is in Haskell, meaning only a simple shim React.Component
is required. This reduces the amount of javascript
required and reduces the need for complex stateful integration with React
.
This is a fully featured TodoMVC in in Haskell and ReactJS using the glazier-react library.
For a live demo, see https://louispan.github.io/glazier-react-examples/
For more details, see the todo example README.md
- Please refer to react docs. You only need to read up to handling events.
- Also read Lists and Keys, and Refs and the DOM.
- Ignore controlled input in Forms. In my experience, controlled input is error-prone and it is better to use it uncontrolled.
- Using uncontrolled input doesn't stop you from subscribing to onChange and obtaining the latest value of the input. Just do not force a render with react
setState
.
- Using uncontrolled input doesn't stop you from subscribing to onChange and obtaining the latest value of the input. Just do not force a render with react
Please read the README.md for a brief overview of glazier.
Glazier.React.Markup
is a StateT monad that enables blaze/lucid style do
notation to markup React elements to render.
bh (strJS "footer") [("className", strJS "footer")] $ do
bh (strJS "span") [ ("className", strJS "todo-count")
, ("key", strJS "todo-count")] $ do
bh (strJS "strong") [("key", strJS "pieces")]
(s ^. activeCount . to (txt . pack . show))
txt " pieces left"
React re-uses Notice from a pool, which means it may no longer be valid if we lazily parse it. However, we still want lazy parsing so we don't parse unnecessary fields.
Additionally, we don't want to block during the event handling.The reason this is a problem is because Javascript is single threaded, but Haskell is lazy. Therefore GHCJS threads are a strange mixture of synchronous and asynchronous threads, where a synchronous thread might be converted to an asynchronous thread if a "black hole" is encountered. See https://github.com/ghcjs/ghcjs-base/blob/master/GHCJS/Concurrent.hs
Glazier.React.Event
uses the event handling idea from the haskell react-flux
library to allow lazy parsing of event safely.
Event handling should only be done via eventHandler
or eventHandlerM
.
eventHandlerM :: (Monad m, NFData a) => (evt -> m a) -> (a -> m b) -> (evt -> m b)
This safe interface requires two input functions:
- a function to reduce Notice to a NFData. The mkEventCallback will ensure that the NFData is forced which will ensure all the required fields from Synthetic event has been parsed. This function must not block.
- a second function that uses the NFData. This function is allowed to block.
mkEventHandler results in a function that you can safely pass into 'GHC.Foreign.Callback.syncCallback1' with 'GHCJS.Foreign.Callback.ContinueAsync'.
Glazier.React
only uses ReactJS
as a thin layer for rendering and registering event handlers. All state and event processing are performed in Haskell, which means only a simple shim React.PureComponent
is required.
Only one shim React component is ever used and the only methods are required are setState
, render
and componentDidUpdate
,
The shim component only has one thing in it's state, a sequence number. This sequence number is only changed with setState
when the Glazier.Gadget
determined that there is a need for re-rendering. This is easy and efficient to determine since Gadget
is the StateT
responsible for changing the state in the first place.
This has the benefits of:
- Only the react shim components with changed haskell state will be re-rendered.
- React is able to efficiently determine if state has changed (just a single integer comparison)
- The shim React component is very simple.
Glazier.React.Model
contain many nuanced concepts of Model.
The Schema
is a template of the pure data for stateful logic (the nouns). It is parameterized by a type variable which specializes it to either an Outline
or 'Model'.
The Outline
is the pure data for stateful logic (the nouns). It may contain 'Outline's of child widgets.
The Outline
does not contain enough information for rendering the child widgets.
The Model
is similar to Outline
, except that it may also contain Gizmos
of child widgets.
It may contain Gizmo
(see below) of other widgets.
The Model
contains enough information to render child widgets, but not this widget.
The Plan
contains the callbacks for integrating with React (the verbs). It also contains a javascript reference to the instance of shim component used for the widget. This reference is used to trigger rendering with setState
.
Model
is basically a tuple of Model
and Plan
. It is a separate data type in order to generate convenient lenses to the fields.
Model
is all that a Window
needs to purely generate rendering instructions.
Frame
is a type synonym of MVar Model
. It is a mutable holder of a copy of Model
. This is so how the official state from Haskell is communicated to the React render
callback. The render
callback will read the latest copy of Model
from the MVar
and pass it to the widget Window
for rendering.
Gizmo
is basically a tuple of Model
and Frame
. It is a separate data type in order to generate convenient lenses to the fields.
This contains everything a widget needs for rendering and state processing.
Most state processing is performed using the pure Model
. The Frame
is only used for the RenderCommand
, to put the latest Model
into the Frame
when re-rendering is required.
MVars
for Frame
s and Callback
s for Plan
s may only be created in IO. Using Free Monads, Glazier.React.Maker
provides a safe way to create them without allowing other arbitrary IO.
The Maker
can also be used create the initial Gizmo
state for the widgets.
The Maker
DSL has an action
type parameter which indicated the type of action that is dispatched by the widget.
The action
type can be mapped and hoisted to a larger action
type, allow for embedding the smaller widget action in larger widget actions.
GHCJS Callback
s has resources that are not automatically collected by the garbage collector. Callback
s need to be released manually. The disposable library provides a safe and easy way to convert the Callback
into a storable SomeDisposable
that can be queued up to be released after the next rendering frame.
disposable allows generic instances of Disposing
to be easily created, which make it easy to create instances of Disposing
for a Plan
of Callback
s, and therefore for the parent container Model
, Gizmo
, and Model
(which may contain other widget Gizmo
s)
The List
widget shows how the disposables can be queued for destruction after the next rendered frame.
A Glazier.React.Widget
is the combination of:
The Maker
instruction on how to create the Model
of that widget from an Outline
:
mkModel :: Outline -> F (Maker Action) Model
The Maker
instruction on how to create the Plan
of that widget:
mkPlan :: Frame Model Plan -> F (Maker Action) Plan
The rendering instructions for that widget:
window:: WindowT (Model Model Plan) ReactMl ()
The state changes from Action
events:
gadget :: Gadget () Action (Gizmo Model Plan) (DList Command)
This is everything you need in order to serialize, deserialize, create, render and interact with a widget.
Glazier.React.IsWidget
is a typeclass that provides handy XXXOf type functions to get to the type of Command
, Action
, Model
, Plan
of the Widget. It also ensures that the Model
and Plan
is an instance of Disposing
.
This is useful for creating widgets that is composed of other Widgets.
Please refer to glazier-react-widget
for documentation on the best practices for creating Glazier.React.Widgets