Skip to content
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

Handling multiple states in event handlers #39

Open
born2defy opened this issue Oct 18, 2015 · 13 comments
Open

Handling multiple states in event handlers #39

born2defy opened this issue Oct 18, 2015 · 13 comments

Comments

@born2defy
Copy link

I am new to Purescript and Thermite, so please pardon me if the answer to this question is obvious. I would like to define a child component which has its own state and event handlers which it uses to validate its input. For example, take a simple text input. When we receive the onChange event from the DOM, we call our custom component action which validates the input and then updates its internal state accordingly (re-rendering itself to display a validation error). When the value is determined to be valid, I want the child component to call an event handler that it received from a parent component through its props. This way the valid value can then be passed back to the parent which can handle it accordingly.
However I am running into issues due to React's EventHandlerContext and Thermite's Action monad both being parameterized by component state.

Currently I can call the parent event handler function directly from the DOM event in the rendered child component (using React.Dom.Props.onChange), and since that function expects any kind of EventHandlerContext this works fine. However, if I want to run the child event handler first to validate the input, I get a type error since the parameterized state for each of the EventHandlerContext operations are different (child state vs parent state). So I can call the parent's event handler with the value, but I can't validate the value in the child component first.

If I choose to handle the onChange event usingThermite's PerformAction with a custom Action type in the child component, I can validate the input but I am unable to call the parent's event handler function for the same reason - the 2 states are different and so the two Thermite.Action operations have different state types. So in this case I can validate the value in the child but am unable to pass it to the parent's event handler.

Is there some way to accomplish both? I would like to encapsulate some simple validation into dumb child components so I can re-use them with different parent components. The parent does not need to worry about validation - it simply gets a valid input event or it gets no event at all, and the child handles all the validation with the user. Is there a simple way to accomplish this that I am overlooking?

@paf31
Copy link
Owner

paf31 commented Oct 18, 2015

I've run into the same problem lately and I've been trying to fix this in the 0.11 branch, so maybe have a look there. The trick I've found to be useful is to keep the effects polymorphic until actually calling createClass at the top level.

@born2defy
Copy link
Author

Thanks for such a fast response. I will take a look at the 0.11 branch. Is there anything that was specifically changed in that branch to deal with this issue that I should focus on first? Since there really is only one parent for each child, and in order to execute the call back you need the parents React context which is already parameterized by the parent's state and props, I was thinking of maybe adding a new Effect type which would contain the callback event and would be type checked against the parents prop and state for safety. That way the callback action can be encapsulated into its own type safe Effect and would no longer interfere with the typed EventHandlerContext of the child. Maybe that is nonsense though because at some point the new Effect would need to be turned into an EventHandlerContext of its own and I'm not sure how that would work.

@paf31
Copy link
Owner

paf31 commented Oct 19, 2015

Nothing specifically changed really, but that branch is a pretty big overhaul of the general architecture, and it got me thinking about composition again, which naturally leads to this issue when the user is given direct access to the state update function (without using the old Action monad for example).

I think Thermite doesn't really need to concern itself with the effects from the purescript-react library, since the user doesn't get access to React's this anyway, so one way would just be to hide those effects with unsafeInterleaveEff internally.

I think the key though is that there are two sets of state and action types: the ones at the top level that the component will be embedded in, passed to createClass, and the component's own state type. They don't need to be the same, and the former can be kept polymorphic for the most part.

@born2defy
Copy link
Author

Yes, I checked it out and it is a pretty significant revision. My previous approach had been drawn from the React docs on facebook's github page, which tend to treat the child components as basically pure rendering functions from the parent, with the parameters being passes as props. Hence the need for the child to be able to execute the call back function that the parent passed into its props in order to notify the parent of any meaningful changes. Looking at this new branch, it seems you are taking a different approach, right? Instead of the child receiving its data and parental connection through props, are you instead structuring that relationship through state? The parent feeds the child a piece of its state (ie through a lens), and the child has access to the parent's action function so it can call updates to the parent in addition to updating itself locally? I haven't had a chance to actually play with it yet, so I am just guessing really based on reading the code.

@paf31
Copy link
Owner

paf31 commented Oct 23, 2015

@born2defy I've simplified the code in the 0.11 branch so that the React effects aren't even used any more (safe because the user doesn't get access to React's this). Hopefully, that should solve this issue.

@born2defy
Copy link
Author

Hi Phil,

I was actually looking over the code last night when the new commit came in, and I definitely appreciate the simplification of the effects. I was playing around with the lens combinators, and they make for a really nice way to distribute pieces of some master state to the child components. One thing I struggled with was figuring out how to use that same lens / prism paradigm when you don't want to tightly couple the children to the parent, as in the case with simple children like parameterized input validators or other re-usable primitives that you may want to freely change out later for some other primitive with the same interface. Since the states and actions must always match when the components are composed with each other, you need a lens or prism to take you from the parent state or action type to the child and vice-versa. How can you do that in a polymorphic way where you don't care about the child's state or actions, but only that the child provides some general interface that you can wire into? In those cases, you really don't want to have to include all these child states in your parent component since they are not in the same domain and you want the ability to swap them out later without rewriting the master state. Perhaps some way to build up the parent state in a monoid which can automatically add the required children's state to the parent through an append operation, so that the children can then access it through an appropriate lens, but it is automatically updated when new children with their own internal stateare added? The same goes for actions - when you just want access to handle some specific event from the child but you don't want to wrap all the possible child events in your parent event (which is what it seems would be necessary in order to create the appropriate prism). Let me know if I am just missing something here, as I am certainly no expert in this arena, and I do appreciate the elegance of the lens approach. I am just wondering if there is a way to generalize it even further that I am just not grasping.

@paf31
Copy link
Owner

paf31 commented Oct 23, 2015

I'm not sure I fully understand the use case you'd like to support. Could you write down some type signatures of what you'd like the components to look like please?

@born2defy
Copy link
Author

I guess essentially what I am trying to do is create some components which have an opaque internal state to handle locally with their own internal actions, but also provide some updates to some parent component, while still being able to combine the parent and child together using the spec monoid. The issue I have is in unifying the action and state types using lenses and prisms, without having to rewrite the state and action of the parent to accommodate the child.

For example, imagine a component called validInt which wraps an HTML input element and only produces valid Ints, firing a validation error message on anything else. The state and actions necessary to apply the validation and re-render the warning message should be fully encapsulated in the component. So it might look something like this:

type ValidIntState = { valid :: Boolean }

data ValidIntAction
  = Valid Int     -- internal 
  | Invalid       -- internal
  | NewValue Int  -- should always be handled by a parent component

When the user tabs out of the input, the event handler is run and the input text is validated. If it succeeds a Valid Int action is dispatched with the parsed Int; otherwise an Invalid action is dispatched. In the performAction function, the Invalid action is used to update the state to false which triggers a re-render displaying the validation error. The Valid Int action is used to update the state to true (re-rendering away any previous validation messages), and then it calls performAction again with a NewValue Int action, which should be handled by some parent component.

The parent component might have an action type which has a constructor designed to handle child updates, like this:

data ParentAction
  = SomeStuff
  | SomeOtherStuff
  | UpdateValue Int  -- generated by some child but handled by parent

I am struggling to figure out how we could make a prism from ParentAction to ValidIntAction sufficient to unify the two action types when they are combined into a single spec using the append operation. It would seem we would have to alter our ParentAction type in order to do so. The same thing goes for the local state of the validInt component. In order to combine the two specs, we would need a lens which can convert from the Parent's state to ValidIntState, but how would we do that without having to include ValidIntState somehow into the Parent's state type?

@paf31
Copy link
Owner

paf31 commented Oct 23, 2015

Why not use the module system with smart constructors to only export the bits of state and actions that you want?

Another option might be to use purescript-exists to existentially hide the bits you don't want the world to access, but in my experience that gets cumbersome very quickly (we tried something similar in Halogen a while ago). You can build the existential bit into the Spec type though (thinking lensily, newtype Hide p a b = Hide { runHide :: exists c. p (a, c) (b, c) } is a Profunctor etc. whenever p is), but I don't know if that's a good solution for Thermite - I'd like to keep the heavy abstractions out if possible.

@born2defy
Copy link
Author

In the most recent version of the 0.11 branch, how would you combine the parent and child specs into a single spec with <>? Which lens would you use to convert from parent state to child state, when they only share one bit of state and the child has unique state that the parent does not share? Or would you have to either 1) build the child's internal state into the parent, modifying the parent or 2) leave them as separate specs and pass a callback function through the child's props in order to handle NewValue updates?

@born2defy
Copy link
Author

Using smart constructors sounds interesting, but I'm not exactly sure how that would look in practice. Would it be possible to provide a simple example?

@paf31
Copy link
Owner

paf31 commented Oct 23, 2015

Would it be possible to provide a simple example?

I can, but probably after I finish off work on the 0.11 branch.

Which lens would you use to convert from parent state to child state, when they only share one bit of state and the child has unique state that the parent does not share?

So something like

type ParentState = Tuple A B
type ChildState = Tuple A C

where only A is shared?

In that case, I'd hide the data constructor for C inside the child component module, and change ParentState to include the full ChildState. There's no way in the current formulation to have the parent include only a piece of the child state - it has to go somewhere. But if I can get the Hide thing to work without paying too much for it in terms of the API, I'll implement that, and then you'd be able to use a combinator like

hide :: Spec eff (Tuple state hidden) props action -> Spec eff state props action

to hide a piece of state, since hide specializes to Spec eff ChildState props action -> Spec eff A props action, for example.

@born2defy
Copy link
Author

Interesting. I am eager to see where this branch ends up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants