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.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 inReact.DOM
converting 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 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?