alexmingoia / purescript-pux

Build type-safe web apps with PureScript.
https://www.purescript-pux.org
Other
565 stars 76 forks source link

Using JS React components #12

Closed dkoontz closed 8 years ago

dkoontz commented 8 years ago

I had looked around for information about using Pux with existing React components but was not successful in finding anything. I have an existing React system and I'd like to start converting it over bit by bit into Purescript. Is there an existing way to have a view render existing components including passing down props? If not, where would be a good place to look to add such functionality?

alexmingoia commented 8 years ago

The easiest way would be to add a renderToReactClass method to Pux that takes a component and returns a react class to use, which takes its initial state from the component props.

In your Pux component you'd have a method that exports a react class:

toReact :: forall eff. Eff eff ReactClass
toReact = do
  app <- start config
  renderToReactClass app.html

and then in your JS React component:

var puxComponent = require('./purs/someComponent.purs').toReact()()

However, using a React component in a Pux component is another thing entirely and most likely not possible without some sort of purescript wrapper.

dkoontz commented 8 years ago

The main use case I have is using existing React components from within Purescript so it sounds like a wrapper is the way to go. Do any major issues spring to mind with that approach? My plan was to replace our top-level component with a Pux component, continue rendering all our sub-components and then proceed down each component chain replacing one layer of React component at a time.

With regards to rendering, is the result of the render function of a React component the same as the VirtualDOM type that Pux produces from the view function? That is, would a reasonable type signature for a React component's render function be something like () -> VirtualDOM or just VirtualDOM assuming I'm not going to try and capture the side effects of the render function in the type signature?

alexmingoia commented 8 years ago

Using React components from within PureScript/Pux might be a little more difficult, depending on the situation.

The approach would be to create an FFI function that has signature: foreign import fromReact :: Array (Attribute a) -> Array (Html a) -> Html a. You'd then need to transform the array of attributes into an object that is passed to React.createElement. Internally, the Attribute type is a JS array ["key", "val"], and children (Html a) are react elements. Does that make sense? Once you've written that function you should be able to embed your JS components in your Pux views, and pass attributes or children to them. It might help to look at the render function in src/Pux.js. Internally, Html a is represented by a React element returned from React.createElement.

dkoontz commented 8 years ago

Yes that is very helpful thanks!

alexmingoia commented 8 years ago

Something like this might work, though I haven't tested it:

foreign import fromReact :: String -> Array (Attribute a) -> Array (Html a) -> Html a
exports.fromReact = function (modulePath) {
  var comp = require(modulePath);
  return function (attributes) {
    var props = attributes.reduce(function (obj, attr) {
      var key = attr[0];
      var val = attr[1];
      obj[key] = val;
      return obj;
    }, {});
    return function (children) {
      return React.createElement(comp, props, children);
    };
  };
};

It'd be great for Pux to have toReact and fromReact.

alexmingoia commented 8 years ago

I may have spoken too soon. During the final render, element event handlers are mutated to inject the action input channel, so this will interfere with any event handlers in the virtual DOM tree returned by the external JS React component. Not sure the best approach to this at the moment.

alexmingoia commented 8 years ago

With Pux v3.0.0 I've added toReact and fromReact methods. You can now use external react components and vice-versa. See the section on React Interop in the guide for information on how to do this. I'd like to get a more comprehensive example published when I have some time.

dkoontz commented 8 years ago

I tested toReact replacing the version I had written myself and it worked great! One question that I was trying to figure out when rolling my own version was how to allow a React component to pass down props into a Pux component. Obviously the main function (or toReact in the case of the docs) can accept parameters, but since that is only called once that wouldn't work for anything dynamic. Receiving props from a parent component feels like it should be handled some sort of propsUpdated function with the same signature as update that could produce a new state. Any ideas on how this could be done?

alexmingoia commented 8 years ago

Instead of

var Counter = PS.Counter.toReact();

it should be

var Counter = function Counter(state) {
  return PS.Counter.toReact(state);
};

That should work if I understand the issue correctly.

dkoontz commented 8 years ago

So what you are suggesting does work for passing params to toReact, I did have to add a parens since toReact was returning a React constructor function and not the component.

function render() {
    const Component = TestComponent.toReact(isEditing)();

    return (
        <div>
            <Component />
        </div>
    );
}

This works and my component can correctly render based on the input, however if isEditing changes, then the entire state of my component is reset because toReact is going to call start again with a new initial state. This is why I was looking for a way to pass along some sort of props-like thing and have them merged into the component's state via a updateViaProps :: ExternalProps -> State -> State type function.

I will work on more use cases to understand if I can accomplish what I'm trying to do without this kind of functionality and come back with hopefully a more concrete example.

alexmingoia commented 8 years ago

Thanks for the update. I'll have to think about this more as well.

dkoontz commented 8 years ago

So thinking about this some more, I believe all of my current use cases would be covered by having a facility to send an action to the top-level component. In this way, when some async or UI thing happens outside the component the React component can forward that to the Pux component via an action. This interaction is ripe for issues if the JS layer sends in an invalid action but it has the nice property of not requiring the component to support any sort of new way of updating. Maybe there is a facility for sending actions to the ReactComponent created by Pux.toReact already but I didn't see it. A custom signal could be passed in to the inputs array of start that comes from FFI and thus could be written to by JS, but it would be really nice to have that already wired up. Does that seem like a good approach?

alexmingoia commented 8 years ago

That seems like a good approach. Have a signal passed to inputs array of start that comes from FFI. I can't think of any cleaner way to do that. Pux could make that easier by somehow doing that for you, but I'm not sure what that would look like. I'm open to pull requests.

dkoontz commented 8 years ago

I have since wired up a new signal that is passed into my top level component and everything works great that way.