-
-
Notifications
You must be signed in to change notification settings - Fork 13
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
HookM code that modifies state can't be passed between components #37
Comments
This looks like it should be valid code to me. The problem is that the HookM code you define in the You can get around it by not using a separate component for the child and include that child within the parent. parentComponent =
Hooks.component \_ input -> Hooks.do
recModalOpen /\ recModalOpenId <- Hooks.useState $ false
Hooks.captures { recModalOpen } Hooks.useTickEffect do
liftEffect $ log $ show recModalOpen
pure Nothing
let
closeModal = do
liftEffect $ log "Closing!"
Hooks.modify_ recModalOpenId (const false)
pure unit
Hooks.pure
$ HH.div
[]
( [ HH.div [ HE.onClick \_ -> Just (Hooks.modify_ recModalOpenId (const true)) ] [ HH.text "open" ] ]
<> guard recModalOpen [ child closeModal ]
)
where
child closeModal =
HH.div
[ HE.onClick \_ -> Just closeModal ]
[ HH.div
[]
[ HH.text "foo"
]
] |
This bug should be renamed to something like, "Defining state-modifying |
@marcusbuffett as @JordanMartinez noticed and we briefly talked about on Slack, the problem is that it’s not safe to pass HookM code between components (though it’s perfectly fine to pass it to hooks). Once you turn hooks into a component you should start using component communication (messages and queries) instead. I’ll need to think a little on how to make this situation either not possible or at least not cause an exception due to the unsafe internal calls to set state. Turns out they’re not hidden behind a safe interface as much as I thought. |
Changed the title from your suggestion @JordanMartinez , and thanks for the alternate solution. I ended up passing a message up from the child component instead, as @thomashoneyman suggested on Slack. Just a bit more boilerplate but got things working now. Appreciate the help guys! |
I think it would be nice if one run code defined in one component in another component. However, I'm not sure whether the resulting implementation would conflict with the test code you would write. If we want to produce a compiler error, would skolem types on |
When you write some code in That's because a component is the interpreter that runs all Hooks code. If your This situation only arises when you use the state hook in one hooks-based component, then use the identifier to write some code that will be evaluated in a different hooks-based component. There's simply no way that this can work, because the identifier is only usable in the component where it was introduced. You can't pass it to another component -- it doesn't exist there. In contrast, you can freely pass around identifiers anywhere within the component. For example, you can nest hooks a dozen deep, passing identifiers down and up the stack of hooks, and it will work just fine because they'll all ultimately be run by one component. It's only a problem once you pass one of them to a different component. This is also the same reason why you don't pass Unfortunately I don't yet have an idea of how to prevent folks from doing this without significantly affecting usability except to say "don't" in the documentation. |
There are essentially two possible approaches here:
|
I’m wondering if something like what ST does would work here? Universally-quantified regions to encapsulate the values at the type level so they only appear in a certain scope. (I haven’t been keeping up with the discussion very well so apologies if I’m off base.) |
It does work, but it also means all hook code must be written with a polymorphic region, with the restrictions that come with that ( |
This has partially been addressed by #44, but I would like to wait a little longer to see if a nicer solution exists before closing the issue altogether. |
So, how would we implement this if we weren't using Hooks and just using regular components? For example, I once tried using |
@JordanMartinez You can get fancier with coercion, but this code demonstrates how you can pass a callback down from a parent to a child component; when the callback is invoked then the parent component will evaluate some code. https://try.purescript.org/?gist=5d21035dabf1512be120dba444411b56 Expand to read the code snippet inline...module Main where
import Prelude
import Data.Foldable (for_)
import Data.Maybe (Maybe(..))
import Data.Symbol (SProxy(..))
import Effect (Effect)
import Effect.AVar as AVar
import Effect.Aff (Aff)
import Effect.Aff.Class (class MonadAff, liftAff)
import Halogen (liftEffect)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.Query.EventSource (affEventSource)
import Halogen.Query.EventSource as EventSource
import Halogen.VDom.Driver (runUI)
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI parent unit body
-- Create an `Aff` function that can be used to trigger an action in one component
-- to be evaluated in another.
mkCallback :: forall st act ps o m. MonadAff m => act -> H.HalogenM st act ps o m (Maybe (Aff Unit))
mkCallback f = do
cbAVar <- liftEffect AVar.empty
_ <- H.subscribe $ affEventSource \emitter -> do
let callback = EventSource.emit emitter f
pure mempty <* liftEffect (AVar.tryPut callback cbAVar)
liftEffect $ AVar.tryTake cbAVar
-- The parent is able to evaluate actions triggered in the child component by
-- passing down a callback
type ParentState = { count :: Int, cb :: Aff Unit }
type Slots = ( child :: forall q. H.Slot q Void Unit )
_child = SProxy :: SProxy "child"
data ParentAction
= Initialize
| Evaluate (forall o m. H.HalogenM ParentState ParentAction Slots o m Unit)
parent :: forall q i o m. MonadAff m => H.Component HH.HTML q i o m
parent =
H.mkComponent
{ initialState: \_ -> { count: 0, cb: pure unit }
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
}
}
where
render state =
HH.div_
[ HH.p_ [ HH.text $ "You clicked " <> show state.count <> " times" ]
, HH.slot _child unit child { cb: state.cb } absurd
]
handleAction :: ParentAction -> H.HalogenM ParentState ParentAction Slots o m Unit
handleAction = case _ of
Initialize -> do
-- make the callback function
mbCallback <- mkCallback $ Evaluate do
H.modify_ \st -> st { count = st.count + 1 }
-- set it in state so it can be passed to child components
for_ mbCallback \cb -> H.modify_ _ { cb = cb }
Evaluate act -> act
-- The child component is able to trigger actions in its parent without sending
-- a message, by receiving a callback passed down.
type ChildInput = { cb :: Aff Unit }
data ChildAction = Run (Aff Unit) | Receive ChildInput
child :: forall q o m. MonadAff m => H.Component HH.HTML q ChildInput o m
child =
H.mkComponent
{ initialState: identity
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, receive = Just <<< Receive
}
}
where
render state =
HH.div_
[ HH.button
[ HE.onClick \_ -> Just (Run state.cb) ]
[ HH.text "Click me (child)" ]
]
handleAction = case _ of
Receive i -> H.put i
Run cb -> liftAff cb As a note, this doesn't have to be direct parent/child communication; the callback that's created here can be passed anywhere (put in global state, passed through a child component to a further child, etc.). But it requires that whatever component calls it can run in |
I think that this issue should be documented (my response here, specifically), but I don't have any plans to update the library to make this possible. If folks want it badly enough I'm willing to consider adding something, but I haven't seen it come up since #44 was merged. |
Having an issue where calling a HookM callback from a child component results in an error like this:
Here's a project that reproduces the problem: https://github.com/marcusbuffett/hooks-issue-example
This is a follow-up from messages on slack where the modal would call the close function but it wouldn't get closed, but it seems in trying to create a reproducible example, I encountered a different issue.
Thanks!
The text was updated successfully, but these errors were encountered: