-- | A binding to based on the -- design. The flux design pushes state -- and complicated logic out of the view, allowing the rendering functions and event handlers to be -- pure Haskell functions. When combined with React's composable components and the one-way flow of -- data, React, Flux, and GHCJS work very well together. -- -- __Prerequisites__: This module assumes you are familiar with the basics of React and Flux. From -- the , you should read at -- least \"Tutorial\", \"Displaying Data\", \"Multiple Components\", and \"Forms\". Note that -- instead of JSX we use a Writer monad, but it functions very similarly so the examples in the -- React documentation are very similar to how you will write code using this module. The other -- React documentation you can skim, the Haddocks below link to specific sections of the React -- documentation when needed. Finally, you should read the -- , in particular the central -- idea of one-way flow of data from actions to stores to views which produce actions. -- -- __Organization__: Briefly, you should create one module to contain the dispatcher, one module for -- each store, and modules for the view definitions. These are then imported into a Main module, -- which calls 'reactRender' and initializes any AJAX load calls to the backend. The source package -- contains some . -- -- __Web Deployment__: 'reactRender' is used to render your application into the DOM. -- Care has been taken to make sure closure with ADVANCED_OPTIMIZATIONS correctly -- minimizes a react-flux application. No externs are needed, instead all you need to do is -- protect the @React@ variable (and @ReactDOM@ if you are using version >= 0.14). The TODO example -- does this as follows: -- -- >(function(global, React, ReactDOM) { -- >contents of all.js -- >})(window, window['React'], window['ReactDOM']); -- -- __Node Deployment__: 'reactRenderToString' is used to render the application to a string when -- running in node (not the browser). To execute with node, you need to get @global.React@ and -- @global.ReactDOMServer@ before executing all.js. The TODO example application does this by -- creating a file @run-in-node.js@ with the contents -- -- >React = require("react"); -- >ReactDOMServer = require("react-dom/server"); -- >require("../../js-build/install-root/bin/todo-node.jsexe/all.js"); -- -- __React Native__: This module also works with -- to create a standalone native applications. When combined with , -- you can even create standalone desktop applications. The workflow is to use 'reactRender' the -- same as web deployment but use the resulting JavaScript file in react-native and/or electron. -- . -- -- __Testing__: I use the following approach to test my react-flux application. First, I use unit -- testing to test the dispatcher and store 'transform' functions. Since the dispatcher and the -- store transform are just data manipulation, existing Haskell tools like hspec, QuickCheck, -- SmallCheck, etc. work well. Note that stores and 'dispatch' work in GHC and GHCJS, so this unit -- testing can be done either in GHC or GHCJS. I don't do any unit testing of the views, because any -- complicated logic in event handlers is moved into the dispatcher and the -- rendering function is difficult to test in isolation. Instead, I test the rendering via -- end-2-end tests using . -- This tests the React frontend against the real backend and hspec-webdriver has many utilities for -- easily checking that the DOM is what you expect. I have found this much easier than trying to -- unit test each view individually, and you can still obtain the same coverage for equal effort. -- The file -- in the source code contains a hspec-webdriver test for the TODO example application. {-# OPTIONS_GHC -fno-warn-duplicate-exports #-} -- ArgumentsToProps is exported twice, once by React.Flux.PropertiesAndEvents and once here module React.Flux ( -- * Dispatcher -- $dispatcher -- * Stores ReactStore , StoreData(..) , mkStore , getStoreData , alterStore , SomeStoreAction(..) , executeAction -- * Views , ReactView , defineControllerView , defineView , defineStatefulView , ViewEventHandler , StatefulViewEventHandler -- * Elements , ReactElement , ReactElementM(..) , elemString , elemText , elemJSString , elemShow , view , viewWithSKey , viewWithIKey , childrenPassedToView , foreignClass , rawJsRendering , transHandler , liftViewToStateHandler , module React.Flux.DOM , module React.Flux.PropertiesAndEvents , module React.Flux.Combinators -- * Main , reactRender , reactRenderToString , exportViewToJavaScript , ArgumentsToProps , ReturnProps(..) -- * Performance -- $performance -- * Depracated , viewWithKey , ReactViewKey ) where import Data.Typeable (Typeable) import Data.Text (Text) import React.Flux.Views import React.Flux.DOM import React.Flux.Internal import React.Flux.PropertiesAndEvents import React.Flux.Combinators import React.Flux.Store #ifdef __GHCJS__ import GHCJS.Types (JSVal, nullRef) import GHCJS.Marshal (fromJSVal) #endif ---------------------------------------------------------------------------------------------------- -- reactRender has two versions ---------------------------------------------------------------------------------------------------- -- | Render your React application into the DOM. Use this from your @main@ function, and only in the browser. -- 'reactRender' only works when compiled with GHCJS (not GHC), because we rely on the React javascript code -- to actually perform the rendering. reactRender :: Typeable props => String -- ^ The ID of the HTML element to render the application into. -- (This string is passed to @document.getElementById@) -> ReactView props -- ^ A single instance of this view is created -> props -- ^ the properties to pass to the view -> IO () #ifdef __GHCJS__ reactRender htmlId rc props = do (e, _) <- mkReactElement id (ReactThis nullRef) $ view rc props mempty js_ReactRender e (toJSString htmlId) foreign import javascript unsafe "(typeof ReactDOM === 'object' ? ReactDOM : React)['render']($1, document.getElementById($2))" js_ReactRender :: ReactElementRef -> JSString -> IO () #else reactRender _ _ _ = error "reactRender only works when compiled with GHCJS, because we rely on the javascript React code." #endif -- | Render your React application to a string using either @ReactDOMServer.renderToString@ if the first -- argument is false or @ReactDOMServer.renderToStaticMarkup@ if the first argument is true. -- Use this only on the server when running with node. -- 'reactRenderToString' only works when compiled with GHCJS (not GHC), because we rely on the React javascript code -- to actually perform the rendering. -- -- If you are interested in isomorphic React, I suggest instead of using 'reactRenderToString' you use -- 'exportViewToJavaScript' and then write a small top-level JavaScript view which can then integrate with -- all the usual isomorphic React tools. reactRenderToString :: Typeable props => Bool -- ^ Render to static markup? If true, this won't create extra DOM attributes -- that React uses internally. -> ReactView props -- ^ A single instance of this view is created -> props -- ^ the properties to pass to the view -> IO Text #ifdef __GHCJS__ reactRenderToString includeStatic rc props = do (e, _) <- mkReactElement id (ReactThis nullRef) $ view rc props mempty sRef <- (if includeStatic then js_ReactRenderStaticMarkup else js_ReactRenderToString) e --return sRef --return $ JSS.unpack sRef mtxt <- fromJSVal sRef maybe (error "Unable to convert string return to Text") return mtxt foreign import javascript unsafe "(typeof ReactDOMServer === 'object' ? ReactDOMServer : (typeof ReactDOM === 'object' ? ReactDOM : React))['renderToString']($1)" js_ReactRenderToString :: ReactElementRef -> IO JSVal foreign import javascript unsafe "(typeof ReactDOMServer === 'object' ? ReactDOMServer : (typeof ReactDOM === 'object' ? ReactDOM : React))['renderToStaticMarkup']($1)" js_ReactRenderStaticMarkup :: ReactElementRef -> IO JSVal #else reactRenderToString _ _ _ = error "reactRenderToString only works when compiled with GHCJS, because we rely on the javascript React code." #endif -- $performance -- -- React obtains high from two techniques: the -- and -- registered on the document. -- -- __Reconciliation__ -- -- To support fast reconciliation, React uses key properties (set by 'viewWithKey') and a -- @shouldComponentUpdate@ lifetime class method. The React documentation on -- talks -- about using persistent data structures, which is exactly what Haskell does. Therefore, we -- implement a @shouldComponentUpdate@ method which compares if the javascript object representing -- the Haskell values for the @props@, @state@, and/or @storeData@ have changed. Thus if you do not -- modify a Haskell value that is used for the @props@ or @state@ or @storeData@, React will skip -- re-rendering that view instance. Note that we are not checking equality, just if the javascript -- object representing a Haskell object has changed, with some special support for pairs and tuples -- of size three. -- -- There is subtle issue: this check only works if the props are not a thunk but are an actual data -- constructor. Consider the following -- -- >data MyStoreData = MyStoreData { -- > myA :: !A -- > , myB :: !B -- > , myC :: !C -- > , myD :: !D -- >} deriving (Show, Typeable) -- > -- >myAview :: ReactView A -- >myAview = defineView .... -- > -- >myStoreView :: ReactView () -- >myStoreView = defineControllerView "my store" myStore $ \myData () -> -- > div_ $ view myAview (myA myData) mempty -- > div_ .... -- -- In @myStoreView@, note that @myA myData@ is passed as the props to @myAview@. So consider the -- situtation when say an action changes @C@ but leaves @A@ unchanged. We would like for the -- rendering of @myAview@ to be skipped, but unfortunately it will be re-rendered. The reason is -- that the props passed to @myAview@ is an unevaluated thunk @myA myData@. Sure, the @A@ -- constructor has not changed and if the thunk is forced it will return this unchanged @A@ data -- constructor, but the @shouldComponentUpdate@ test does not do any computation or evaluation, it -- just checks if the passed in javascript object is the same as it was the last time the view was -- rendered. We can fix this by forcing the thunk before passing it to 'view', which I do via bang -- patterns. Instead of ever calling 'view' directly from a rendering function, for each -- 'ReactView' I create a combinator as follows: -- -- >myAview_ :: A -> ReactElementM handler () -- >myAview_ !a = view myAview a mempty -- > -- >myStoreView :: ReactView () -- >myStoreView = defineControllerView "my store" myStore $ \myData () -> -- > div_ $ myAview_ (myA myData) -- > div_ .... -- -- Note the bang pattern on the @a@ parameter to @myAview_@. What now happens is that the bang pattern -- forces the thunk @myA myData@ to turn into the @A@ data constructor. If an action does not edit the @A@ portion -- of the store data, this will still be represented by the same javascript object as before and -- React will not re-render the @myAview@. -- -- Now consider another situtation where you would like a view that takes A and B. -- -- >myAandBview :: ReactView (A, B) -- >myAandBview = defineView .... -- > -- >myAandBview_ :: A -> B -> ReactElementM handler () -- >myAandBview_ !a !b = view myAandBview (a, b) mempty -- > -- >myStoreView :: ReactView () -- >myStoreView = defineControllerView "my store" myStore $ \myData () -> -- > div_ $ myAview_ (myA myData) -- > div_ $ myAandBview_ (myA myData) (myB myData) -- > div_ .... -- -- Again, if you have an action that just changes @C@ you would like @myAandBview@ to not be -- re-rendered. With the simple javascript object check, it would be re-rendered because the props -- are a tuple and the Haskell value (and thus javascript object) for the tuple is being recreated each -- time @myStoreView@ is rendered. To overcome this obstacle, @react-flux@ contains special code to check pairs -- and tuples of size three. If the props are a pair or a tuple of size three, the components of -- the tuple will be compared to see if they are the same javascript object. Thus similar to the -- above we need to make sure each component of the tuple is not a thunk but a data constructor, -- which happens via the bang patterns in @myAandBview_@. The end result is that if an action just -- changes @C@ or @D@ and leaves @A@ and @B@ unchanged, the above code will cause React to not -- re-render @myAandBview@ because the two components of the pair are forced and are still the same -- unchanged data value/javascript object. You can see this in action inside the test suite if you -- would like an example. -- -- So far we have been focusing on making sure the new props are not a thunk by forcing it before -- passing it into 'view'. But we also need to make sure the initial props are not a thunk. This -- is not quite as bad since the check will only fail the next time a re-render occurs and after -- that everything will be OK so we will still mostly skip re-rendering, but is still a small -- annoyance. There are several ways to fix this, but the easiest is to add bang patterns to the -- definition of @MyStoreData@. If you scroll up you can see that each member of @MyStoreData@ has -- a bang pattern. Thus when an action does change @A@, whatever a new value is set into @myA@, it -- will not be a thunk but an actual data constructor. Then the initial props passed into the view -- will not be a thunk. -- -- In summary, you should follow these rules: -- -- 1. Use bang patterns on each member in your store data. In fact, once GHC 8 is released, I -- plan on turning on the new @StrictData@ extension and then all these bang patterns can be -- dropped. -- -- 2. Try and keep your view parameters as part of the store that will be unchanged by some -- actions. Use tuples of size two or three to combine multiple parts of the store data or even -- data from multiple stores. (Tuples of larger size could be supported without much effort if -- required.) -- -- 3. For each view, make a combinator with a underscore suffix which uses bang patterns to force -- the props before passing it to the 'view' function. -- -- __Events__ -- -- For events, React registers only global event handlers and also keeps event objects (the object -- passed to the handlers) in a pool and re-uses them for successive events. We want to parse this -- event object lazily so that only properties actually accessed are parsed, but this is a problem -- because lazy access could occur after the event object is reused. Instead of making a copy of -- the event, we use the 'NFData' instance on 'SomeStoreAction' to force the evaluation of the store -- action(s) resulting from the event. We therefore compute the action before the event object -- returns to the React pool, and rely on the type system to prevent the leak of the event object -- outside the handlers. Thus, you cannot "cheat" in the 'NFData' instance on your store actions; -- the event objects dilerbertly do not have a 'NFData' instance, so that you must pull all your -- required data out of the event object and into an action in order to properly implement 'NFData'. -- Of course, the easiest way to implement 'NFData' is to derive it with Generic and DeriveAnyClass, -- as @TodoAction@ does above. -- $dispatcher -- The dispatcher is the central hub that manages all data flow in a Flux application. It has no -- logic of its own and all it does is distribute actions to stores. There is no special support -- for a dispatcher in this module, since it can be easily implemented directly using Haskell -- functions. The event handlers registered during rendering are expected to produce a list of 'SomeStoreAction'. -- The dispatcher therefore consists of Haskell functions which produce these lists of -- 'SomeStoreAction'. Note that this list of actions is used instead of @waitFor@ to sequence -- actions to stores: when dispatching, we wait for the 'transform' of each action to complete -- before moving to the next action. -- -- In the todo example application there is only a single store, so the dispatcher just -- passes along the action to the store. In a larger application, the dispatcher could have its -- own actions and produce specific actions for each store. -- -- >dispatchTodo :: TodoAction -> [SomeStoreAction] -- >dispatchTodo a = [SomeStoreAction todoStore a]