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.Dynamic functions 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 in React.DOM converting child lists to spread arguments when calling React.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 Unit
With 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 -> rawElems
The 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 } -> ReactElements
That 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?
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.DOMandReact.DOM.Dynamicto separate components that take statically-known children vs components that take dynamically-computed child lists. This is nice, but there are two potential issues: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.I had a thought that these issues could be addressed by emulating lucid's trick of abusing
donotation 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
ReactElementtype in purescript, one could use a wrapper that represents arrays ofReactElementvalues. Elements in the array are tagged, so that static children are represented by plainReactElementvalues, while dynamically-generated children are represented as a nested array.With this type only one code path is required for invoking the javascript
React.createElementfunction: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
ReactElementstype can be made user-friendly by avoiding exposing theTaggedElementtype or its constructors directly to users. Instead, library-defined components are given in the form ofReactElementsvalues, and these are composed into child lists using aBindinstance.(This could be a bit simpler with an applicative-do feature ;)
A user could write code that looks like this:
When the user wants to dynamically generate an array of elements, they would use a smart constructor
elementsto transform an array ofReactElementsvalues into a single value.The
elementsimplementation flattens the input elements array so that there are always exactly two levels of nesting inReactElementsvalues.Using
elements, a user can mix static and dynamic children as desired:This opens up another possibility of implementing the
elementsconstructor in such a way as to statically enforce the requirement that dynamically-generated elements should have akeyprop:That would just require a function to modify a given
ReactElementto add thekeyproperty 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?