joelburget / react-haskell

React bindings for Haskell
MIT License
351 stars 26 forks source link

first class classes #8

Open joelburget opened 9 years ago

joelburget commented 9 years ago

It's currently impossible to render a class from a ReactT. This should be possible with locally or something.

In the best of all worlds you could do something like:

div_ $ do
    someClass
    span_ "this is not a class"

There are a few problems though...

johncant commented 9 years ago

Can we have a composition tag reactClass_ to bridge the gap between ReactT and ReactClass in an obvious way?:

haskell div $ do reactClass someClass span_ "this is not a class"

johncant commented 9 years ago

^ I see your point about createClass living in the IO monad from trying to use the above example

It looks like the Javascript render function on each React class is being short-circuited and it needs to work in order for a React class to work when instantiated using createElement(someClass). Also, it looks like the state is currently associated with each class, whereas it should be associated with each class instance, which should all get created by React. React already has a place for storing state, so wouldn't it be correct to store the Haskell state as a key inside the JS state?

About to do some more hacking........

johncant commented 9 years ago

Where do animations and transitions fit into Javascript React?

joelburget commented 9 years ago

Yeah, I think something like reactClass_ is the right way to approach rendering classes. I might want to reuse locally.

With 0.13b1 React.createClass is no longer required, which makes it less awkward to move class creation out of the IO monad.

I'm currently thinking of classes in react-haskell slightly differently than classes in React proper. Each one is sort of a standalone root like you might see in flux controller-views. The idea being you can isolate different parts of the tree so the whole thing doesn't have to rerender.

So react-haskell is much more prescriptive than regular React because it builds in (soon) controller-view components, transitions, and animations. I think in the future I'd like to separate these different things into their own packages (along with routing, etc), but for now it's most convenient to develop them in the same package.

As far as state vs props, they're operationally the same for top level components, but I think it's slightly clearer to use props.

Was that all reasonable?

johncant commented 9 years ago

I've been working on a branch which uses a reactClass_ as a starting point - it can always be done away with later. The unmonadic class creation helps massively. Although, couldn't we just not use the IO monad, since class creation is effectively instant and doesn't have any side effects (I couldn't think of any)?

But surely you still want to be able to compose them? Does a state change in a component cause all of its parent components to rerender?

I think your approach to these extensions is reasonably sound. With animations, I sort of abandoned them in order to get composing classes working. I'll hopefully be able to bring them back in as an extension in a separate module, but this package. I'll need to do that before submitting a pull request because otherwise my hacking would decrease the functionality in this repo.

joelburget commented 9 years ago

couldn't we just not use the IO monad, since class creation is effectively instant and doesn't have any side effects

Yep!

But surely you still want to be able to compose them?

Yes. Here's what I'm thinking. reactClass_ and locally are almost equivalent, but the former takes a ReactClass and the latter takes a ReactT.

With locally, you're embedding a "dom fragment". It has no embedded knowledge. The class is still responsible for handling transitions and animations.

With reactClass_, you're embedding a class, which has embedded knowledge. The child class handles its own events like normal. But the parent class also handles events the child class emits. I think this means we need to add another variable to ReactClass so it has a notion of both internal and external signals - ie those signals which it handles and those it emits. Emitting a signal only happens in response to handling a signal (by the way, I'm being sloppy with the terms "signal" and "transition" - I mean the same thing by both). render only renders classes that emit Void - ie never emit signals.

Does a state change in a component cause all of its parent components to rerender?

No. In the formulation I'm picturing a class means two different, but related things:

  1. It's a conceptual whole. An abstraction. Examples: search box, map, etc.
  2. It batches events and animations. It's a hub for state changes that might be contained. In the map example, most interactions and animations are probably handled within that class, without ever alerting the map's parent of user interaction.

My observation is that work (events and animations) can often be contained within an abstraction. Like in the map example - we can contain all that work to within the map. I propose intentionally conflating the two meanings of classdom in ReactClass because more often than not they roughly coincide.

Now, we can limit work to within a class except for when the child emits an event.

joelburget commented 9 years ago

Two things I need to think about / come to terms with:

1. uni-directional data flow

In the flux architecture uni-directional data flow feels like an improvement over plain React. We have something similar-ish at the moment, but only because we can't nest classes (which is definitely a bad thing). React-haskell signals and flux actions are roughly the same. They transition the store (there's only one in react-haskell, it's the top-level state).

Now, it feels like an improvement over the current state of affairs to be able to nest classes, selectively emitting events from child classes, hiding irrelevant state. However, if you squint we've arrived right back at the original React architecture, with props being passed in and state within the class. The difference is we're emitting events rather than taking callbacks. But still, I guess that means we've lost uni-directional data flow, since we need to handle signals at every level in the hierarchy.

I don't think the situation is quite as bad as that, which I can elaborate on later when I've thought about it more and am less tired.

2. hidden state

Nesting classes also (if we do it wrong) introduces hidden state, which is definitely not good. One of my design requirements is that you can serialize the entire state of the app. Preferably you can serialize the entire history of the app. Every transition that's ever happened. Would make debugging so much easier.

Note: I've taken a small step away from this world with animations. They're treated as inconsequential state which would be ignored in serialization. I think that's okay.

Where I'm looking for inspiration:

johncant commented 9 years ago

I took a look at this to get more insight: https://github.com/ianobermiller/nuclearmail/blob/master/src/js/ThreadView.js and made some notes mainly to help my own understanding:

React:

Flux:

End of notes

Is hidden state required?

If you want to let people use react-haskell in a non-Flux-like way, composing classes, then yes. If you only want to only let people use a Flux-like architecture, then no, unless performance of rerendering the whole React DOM or copying most of a large immutable object is likely to become a problem, in which case yes.

I'd suggest allowing hidden state to exist, and leaving it up to the application as to whether to use it. I'd advocate having a ReactClass that mapped as closely as possible to a JS React Class, and exporting an alternative ReactClass in a module maybe called React.Flux where state could always be (), thereby banning hidden state. An application that used a flux architecture with react-haskell could then more conveniently use React.Flux.ReactClass, not have to worry about state or child handlers, and always be able to serialize its entire state.

This breaks transitions a tiny bit though, because they can no longer just act on the state of the component, instead they have to act the state of some global datastore.