-
Notifications
You must be signed in to change notification settings - Fork 65
Description
Hello everyone! I am just getting into purescript, and I am looking forward to having another functional language in my life.
Trying out purescript-react-native, I immediately ran into problems with React complaining about missing key props in child elements. I see from the code in purescript-react and from the discussion in #53 that purescript-react has addressed this issue by providing the modules React.DOM and React.DOM.Dynamic to separate components that take statically-known children vs components that take dynamically-computed child lists. This is nice, but there are two potential issues:
- I predict that I will forget to use
React.DOM.Dynamicfunctions when I dynamically generate child lists, and I will forget to put key props on those child elements. When this happens I will not get warnings from React due to the functions inReact.DOMconverting child lists to spread arguments when callingReact.createElement. - The distinction between components that take static vs dynamic child lists prevents mixing static and dynamic children in a single child list - which is something that React supports.
I had a thought that these issues could be addressed by emulating lucid's trick of abusing do notation to sequence Html elements.
(As I mentioned, I am a newcomer to purescript. I apologize if this is well-trodden ground, or if this post is otherwise unhelpful.)
My thinking is that instead of using the raw ReactElement type in purescript, one could use a wrapper that represents arrays of ReactElement values. Elements in the array are tagged, so that static children are represented by plain ReactElement values, while dynamically-generated children are represented as a nested array.
data TaggedElement = StaticElement ReactElement
| DynamicElements (Array ReactElement)
data ReactElementsImpl a = ReactElementsImpl (Array TaggedElement) a
type ReactElements = ReactElementsImpl UnitWith this type only one code path is required for invoking the javascript React.createElement function:
function createElement(class_) {
return function(props) {
return function(children) {
var unwrappedChildren = children.map(c => c.value0);
return React.createElement.apply(React, [class_, props].concat(unwrappedChildren));
};
};
}Child element lists are unconditionally passed as spread arguments - but because dynamically generated child lists are in nested arrays, those lists map to array arguments to createElement, which causes React to infer that children in those arrays are dynamically-generated.
The ReactElements type can be made user-friendly by avoiding exposing the TaggedElement type or its constructors directly to users. Instead, library-defined components are given in the form of ReactElements values, and these are composed into child lists using a Bind instance.
instance functorReactElementsImpl :: Functor ReactElementsImpl where
map f (ReactElementsImpl es x) = ReactElementsImpl es (f x)
instance applyReactElementsImpl :: Apply ReactElementsImpl where
apply (ReactElementsImpl as f) (ReactElementsImpl bs x) = ReactElementsImpl (as <> bs) (f x)
instance bindReactElementsImpl :: Bind ReactElementsImpl where
bind (ReactElementsImpl es x) f =
case f x of
ReactElementsImpl es' y -> ReactElementsImpl (es <> es') y(This could be a bit simpler with an applicative-do feature ;)
A user could write code that looks like this:
render _ = return $
div' $ do
span' (text "This is an example")
span' (text "of a construct that `do` notation was arguably not intended for")When the user wants to dynamically generate an array of elements, they would use a smart constructor elements to transform an array of ReactElements values into a single value.
elements :: Array ReactElements -> ReactElements
elements es = ReactElementsImpl [DynamicElements (flat es)] unit
where
flat reArray = do
reactElements <- reArray
case reactElements of
ReactElementsImpl taggedElements _ -> do
taggedElement <- taggedElements
case taggedElement of
StaticElement rawElem -> [rawElem]
DynamicElements rawElems -> rawElemsThe elements implementation flattens the input elements array so that there are always exactly two levels of nesting in ReactElements values.
Using elements, a user can mix static and dynamic children as desired:
render _ = return $
div' $ do
span' (text "This is an example")
span' (text "of mixed strict and dynamic child elements")
ul' $ do
li' (text "first item")
elements $ (\n -> li' (text ("list item #" ++ show n))) <$> 1..10
li' (text "last item")This opens up another possibility of implementing the elements constructor in such a way as to statically enforce the requirement that dynamically-generated elements should have a key prop:
elements :: Array { key :: String, element :: ReactElements } -> ReactElementsThat would just require a function to modify a given ReactElement to add the key property after-the-fact.
I have done just enough experimentation with this idea to verify that the code above type-checks. I wanted to get some feedback to see if people like this idea before trying for a working implementation. So, what do you all think?