-
Notifications
You must be signed in to change notification settings - Fork 65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Alternative class construction #129
Conversation
This removes `createClass` and `spec` as well as the dependency on `create-react-class` in favor of `component` and `pureComponent` which creates constructors that inherit from `React.Component` and `React.PureComponent` in an idiomatic way for React. This also obviates the need for some machinery like `ref` handling, which can be implemented with `purescript-refs` instead.
src/React.purs
Outdated
=> RowLacks "key" props | ||
=> ReactRender render | ||
=> String | ||
-> ReactClassConstructor (Record props) (Record state) render eff r |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that I've required both props and state to be Records, as this is what React expects. I've removed the state boxing completely.
-- | Then the `displayName` will be set up to `HelloWorldCls` | ||
foreign import createClassStateless :: forall props render. | ||
ReactRender render => | ||
(props -> render) -> ReactClass props |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know why these functions exist since this can be implemented with PureScript functions. If we do want to add this back, then they can be implemented in PureScript on top of pureComponent
.
@@ -455,10 +378,6 @@ foreign import createElementTagName :: forall props. | |||
foreign import createElementTagNameDynamic :: forall props. | |||
TagName -> props -> Array ReactElement -> ReactElement | |||
|
|||
-- | Create a factory from a React class. | |||
foreign import createFactory :: forall props. | |||
ReactClass props -> props -> ReactElement |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
createFactory
is a deprecated API and the signature here is wrong anyway.
src/React/DOM/Props.purs
Outdated
@@ -638,6 +620,10 @@ onWheel :: forall eff props state result. | |||
(Event -> EventHandlerContext eff props state result) -> Props | |||
onWheel f = unsafeMkProps "onWheel" (handle f) | |||
|
|||
ref :: forall eff props state result. | |||
(Nullable Foreign -> EventHandlerContext eff props state result) -> Props | |||
ref f = unsafeMkProps "ref" (handle f) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've change ref
to take a Foreign
. Ref
was a completely opaque data type, and the only way to validate it was by casting it to Foreign
anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason is that I would like to avoid a dependency on foreign
which pulls in a lot of other heavy dependencies. I think it's desirable to keep this package as light as we can, since it's a reasonable use case to use PureScript for one-off React components, in which case DCE can be important.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the user doesn't use ref functionality, won't the dependency be eliminated altogether in that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well if they're using purs bundle
, or if they're not using the React module as an entry point, then yes.
But I still don't see the reason to add a foreign
dependency just for the Foreign
type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not a hill I'm gonna die on, I just think it's weird to export an API with an opaque type and provide no way for working with it :P.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using toForeign
isn't a big deal, but we should note it.
I've reverted the |
Thanks for working on this! I think the alternative class proposal is great. It is definitely nice to avoid I've tested out the changes a bit and have two comments. First, I think it would be handy to continue to support React's functional components. If I understand it correctly, I think that functional components don't inherit from If we decide to go with an implementation not using class ReactCreateElement a
instance classReactCreateElement :: ReactCreateElement (ReactClass (Record props))
instance functionReactCreateElement :: ReactRender render => ReactCreateElement (Record props -> render)
createElement :: forall props a. ReactCreateElement a => a -> props -> Array ReactElement -> React.ReactElement
createElement = ... Second, and this is a small point, but should the display name be moved into an optional property? I definitely see the utility in making it as a parameter to Thanks again for working on this proposal! |
It would just be type Props = ...
myComponent :: Props -> Array ReactElement -> ReactElement
myComponent props children = D.div ... Since we aren't using JSX, there's not really a reason to create a
The above definition could be wrapped like so: -- Omitting types because constraints
stateless name pureRender = component name \this ->
pure { state: {}, render: pureRender <$> getProps this <*> getChildren this }
myComponentClass :: ReactClass Props
myComponentClass = stateless "MyComponent" myComponent The main problem with overloading
It's optional because React can infer the name from the name of the class definition in JS. "Classes" defined in PS however don't have a name since they are built from an anonymous function. You will want a name if you want to be able to debug your code using the traces React generates. If someone doesn't want to worry about names, it's easy enough for them to define: hardToDebugComponent = component "" |
An additional point against overloading createElement (myFoo foo) props children I think it makes more sense to opaquely lift a function to data ReactConstructor props state = ReactConstructor (ReactThis props state -> Eff ...)
data ReactClass props
= LifecycleConstructor (Exists (ReactConstructor props))
| PureRender (props -> Children -> ReactElement)
data ReactBoundComponent props = BoundProps (ReactClass props) props
data ReactElement
= ReactElementComponent (Exists ReactBoundComponent)
| ReactElementString String
| ReactElementMulti (Array ReactElement)
| ReactElementDOM ...
| ... |
I agree with Nate. I am for simplifying the types and usage, rather than complicating them with additional classes. The closer to idiomatic React usage the better (imo). |
@natefaubion Thanks for the information regarding the functional components. It definitely makes sense in terms of just writing a PureScript function and calling it directly from another react component. However, one aspect I would like to better understand is how this works in JSX. For example, given the following JSX: var foo =
<div>
<Bar name="a"></Bar>
</div>
;
function Bar({ name }){
return <div>{name}</div>;
} Babel transpiles to: "use strict";
var foo = React.createElement(
"div",
null,
React.createElement(Bar, { name: "a" })
);
function Bar(_ref) {
var name = _ref.name;
return React.createElement(
"div",
null,
name
);
} In the above, the function Pertaining to the type class overloading, I think your data-type proposal is a good one -- I am personally open to either approach. I see that for the we added (and made Object.defineProperty(Constructor, 'name', {
value: ctrFn.name
}); Doing this will then allow react to pick up the name of the PureScript function that is passed to hello :: React.ReactClass { name :: String }
hello = React.component "Hello" hello'
where
hello' this = ... would show I don't know if this is a good or bad approach, but thought it might be worth mentioning. |
React does not diff children of different component types. If I replace We can definitely support this pattern "unboxed" like in React. My concern is that React attaches referential identity to these things, which is a semantic that PureScript function don't have. I think it's a better pattern in PureScript to explicitly lift these things to a
FWIW, I'm not suggesting that we should use explicit PS ADTs to model anything, just that it's a useful way to think about how React elements work.
This is a personal opinion, but I don't care for this kind of magic in a PS API. I understand not everyone feels this way. In JS, stateful components will always have a display name, since you must define a |
Thanks for the rundown @natefaubion. Very helpful. Good point about React attaching a referential identity to the functional components. Given this, I agree with you that perhaps Having an explicit |
Something else I'm trying out, internally (after removing the RowLacks constraints): createElement
:: forall required given optional children
. Union given optional (key :: String | required)
=> ReactClass (Record (children :: children | required))
-> Record given
-> children
-> ReactElement
createElement = createElementImpl This allows for typed children, and for an optional "key" prop. |
One problem with this is the polymorphism-in-record problem we always hit. |
I'm not sure if it was discussed before, but could we take inspiration from https://github.com/reasonml/reason-react ? |
As far as I can tell, it's basically untyped: |
I'm really interested in this PR. Is there something blocking it? |
For what it's worth, I have been using this formulation for a few weeks and it works quite nicely. |
I think these changes look good. Thank you for all of the work on this. @natefaubion is there anything that you are trying out that you would like to include in this PR? @paf31 just checking in to see if you had any suggestions regarding this PR. Thanks! |
@ethul I haven't had a chance to use the above |
After looking into it some more, I don't think that transparently proxying the children works because it isn't parametric over arrays. React does some ad hoc magic to collapse empty children to null, and to remove the wrapper for single children. I'm open to suggestions about the Edit: To expand, you'd have to do implicit boxing to keep it parametric like we've done in the past with state, and I don't want to do that since it makes interop weird. |
@natefaubion Agreed that deferring the constraints to Regarding |
@ethul Yes, I think one option is: createElement
:: forall required given optional children
. Union given optional (key :: String | required)
=> ReactClass (Record (children :: Children children | required))
-> Record given
-> Array children
-> ReactElement
createElement = createElementImpl
createLeafElement
:: forall required given optional children
. Union given optional (key :: String | required)
=> ReactClass (Record required)
-> Record given
-> ReactElement
createLeafElement = createLeafElementImpl For reference: https://flow.org/en/docs/react/children/ |
@natefaubion Looks like that option could work out nicely. Thanks for the reference and code snippets. Are you willing to incorporate these latest ideas into this PR? |
@ethul Sure, I'll give it a try. |
Great! Thanks
…On Sat, Feb 24, 2018 at 13:52 Nathan Faubion ***@***.***> wrote:
@ethul <https://github.com/ethul> Sure, I'll give it a try.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#129 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAVYy98LVUrCZn723OC9V5J3mbN7iDF4ks5tYFpvgaJpZM4QrACz>
.
|
The createElement :: forall required given optional.
Union given optional (key :: String | required) =>
ReactClass { children :: Children | required } ->
Record given ->
Array ReactElement ->
ReactElement
createElement = createElementImpl
createLeafElement :: forall required given optional.
Union given optional (key :: String | required) =>
ReactClass { | required } ->
Record given ->
ReactElement
createLeafElement = createLeafElementImpl You can still provide other typed children parametrically using testChildren :: ReactClass { label :: String, children :: Children }
testChildren = component "TestChildren" \this -> do
let
render = do
props <- getProps this
pure $ div'
[ text props.label
, div' (childrenToArray props.children)
]
pure { state: {}, render }
testTypedChildren :: ReactClass { label :: String, children :: Int -> ReactElement }
testTypedChildren = component "TypedTestChildren" \this -> do
let
render = do
props <- getProps this
pure $ div'
[ text props.label
, div' [ props.children 42 ]
]
pure { state: {}, render }
test :: ReactElement
test = div'
[ createElement testChildren { label: "Hello", key: "test" }
[ div' [ text "World" ]
, div' [ text "Children" ]
]
, createLeafElement testTypedChildren
{ label: "Typed"
, children: \i -> text (show i)
}
] |
So playing with this some more, what you really need to make it type safe is: class ReactPropFields (required :: # Type) (given :: # Type)
instance reactPropFields ::
( Union given optional (key :: String | required)
, Union optional leftover (key :: String)
) =>
ReactPropFields required given
createElement :: forall required given.
ReactPropFields required given =>
ReactClass { children :: Children | required } ->
{ | given } ->
Array ReactElement ->
ReactElement
createElement = createElementImpl But this again prohibits you from having polymorphic props. Edit: It doesn't prevent you from having polymorphic props, but you do need to add a type annotation. testPolyProps :: forall r. ReactClass { label :: String | r }
testPolyProps = component "PolyProps" \this -> do
let
render = do
props <- getProps this
pure $ div'
[ text props.label
]
pure { state: {}, render } createLeafElement (testPolyProps :: ReactClass { label :: String, foo :: Int })
{ label: "Hello"
, foo: 42
} |
Thanks for making these changes. I think it works to have a monomorphic,
opaque, Children type. The class looks interesting however. I will dig into
this update and do some testing. Thanks again!
…On Sat, Feb 24, 2018 at 20:26 Nathan Faubion ***@***.***> wrote:
So playing with this some more, what you really need to make it type safe
is:
class ReactPropFields (required :: # Type) (given :: # Type)
instance reactPropFields ::
( Union given optional (key :: String | required)
, Union optional leftover (key :: String)
) =>
ReactPropFields required given
createElement :: forall required given.
ReactPropFields required given =>
ReactClass { children :: Children | required } ->
{ | given } ->
Array ReactElement ->
ReactElement
createElement = createElementImpl
But this again prohibits you from having polymorphic props.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#129 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAVYy_u9DqKSm-4h6Vmtuuq_BqOVzxa6ks5tYLa4gaJpZM4QrACz>
.
|
@ethul This is probably a little more clear, but generates an extra constraint argument. class ReactPropFields (required :: # Type) (given :: # Type)
instance reactPropFields ::
( Union given optional all
, Union optional leftover (key :: String)
, Union required (key :: String) all
) =>
ReactPropFields required given This just makes the |
I apologize for the delay. I think these changes look great. I had a chance to write a few components with these updates and the module is quite nice to use. The update to |
Sorry, I should have clarified. The updated ReactPropFields is equivalent, just phrased with an extra Union constraint. I should probably update it with a |
Thanks for making these changes. I think it makes sense to address #131. If
you’re game to include it into this PR, please feel free. We can also do a
separate PR before making the next major release if that makes more sense.
I also wonder if there are any other breaking changes we should bring in
before doing the next major release.
…On Sat, Mar 3, 2018 at 13:29 Nathan Faubion ***@***.***> wrote:
@ethul <https://github.com/ethul> How do you feel about addressing #131
<#131>? I
think this PR is good to go, but I think we should address it before making
another breaking release.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#129 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAVYywUevi_sfbzkFlGiT8B7bstDNfOxks5tauD8gaJpZM4QrACz>
.
|
I think I'd like to merge this PR as is and make a new one for the render changes. |
Sounds good.
…On Sat, Mar 3, 2018 at 21:55 Nathan Faubion ***@***.***> wrote:
I think I'd like to merge this PR as is and make a new one for the render
changes.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#129 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAVYy5MxEQ9pRLBCZrkHYfFfphrMmq6cks5ta1exgaJpZM4QrACz>
.
|
Thanks again! |
This removes
createClass
andspec
as well as the dependency oncreate-react-class
in favor ofcomponent
andpureComponent
whichcreates constructors that inherit from
React.Component
andReact.PureComponent
in an idiomatic way for React. This also obviatesthe need for some machinery like
ref
handling, which can beimplemented with
purescript-refs
instead.