How component function is returning an effectful computation?

Hi,
I’ve come across this comment in react-basic-hooks, which says

Creating components is effectful because React uses the function instance as the component’s “identity” or “type”

I thought only using component (calling root.Render) is effectful as it uses function instance (along with dom manipulation and state management).

maybe it a trivial detail in the whole implementation of react basic hooks.

I’m curious to know how component creation is effectful as component function is only calling React.createElement inside it. And React.createElement doesn’t seems effectful.

React uses a function’s runtime reference as a component identity when diffing. This is important because of how polymorphism works in PureScript. Without the Effect, you can poke a hole in the type system and implement unsafeCoerce. It’s the same reason why creating a mutable Ref needs to be effectful (see the “value restriction” in ML).

See this trypurs demonstration of how to cast a String to an Int through a bad Ref.

Well, React components (hooks) have all the same mutability issues. You can allocate both refs and state which persist between invocations of the component. Because React uses a function’s referential identity to key these states, if you didn’t have it in Effect, then you could type a component polymorphically and instantiate it with different types in different rendering cycles. This is an issue because polymorphism is runtime irrelevant. That is, the same function reference is used regardless of what type you might use with it. Putting it in Effect (ie, staging component instantiation) forces the component to have a monomorphic type during rendering, preserving type safety.

1 Like

Thanks @natefaubion , didn’t know about value restriction before.

So even though React.createElement itself is not effectful, we need to use Effect Monad to solve below scenario.

counter :: forall a. Effect ({val:: a, set :: a -> a, get :: a -> String} -> JSX)
counter  = Hooks.component "GenericComponent" \props -> Hooks.do
 state /\ setState <- Hooks.useState props.val

 let
   handleClick = \_ -> setState props.set

 pure $
   DOM.div
     { onClick: Events.handler DOM.Events.button handleClick
     , children: [ DOM.text (props.get state) ]
     }

mkApp :: Component Unit
mkApp = do
  counter' <- counter
  Hooks.component "App" \_ -> Hooks.do
    flag /\ setFlag <- Hooks.useState true
    pure
      ( DOM.div_
          [ DOM.button
              { onClick: Events.handler DOM.Events.button (const $ setFlag not)
              , title: "Toggle type"
              }
          , if flag then counter' {val: "0", set: \x -> x<>"1", get: identity}
               else counter' {val: 0, set: \x -> x+1, get: show} -- error: could not match type Int with type String
          ]
      )

in above code using same function for different types is not possible as type inferring will happen by bind function.

So this could be solved with any monad that exposes value only through bind? (although Effect has the advantage of magic do). Or should we consider React.createElement operation effectful in nature ?

This particular issue is the intersection of:

  • Observable referential mutation.
  • Polymorphism.
  • Type erasure (runtime irrelevance of type arguments).

So if you can observe a reference, give it a polymorphic type, and instantiate that polymorphic type without affecting the reference, you have a hole in the type system. This can be fixed through:

  • Runtime relevance of type arguments. If all type arguments result in a “real” function argument, then type instantiation would result in a new reference.
  • The value restriction. If you can only ascribe polymorphic types to syntactic functions, then references can’t have polymorphism. This is a gray area for hooks components. In TypeScript, hooks components are syntactic functions, and thus are unsound (but TS is unsound anyway). In some ML with the value restriction, you’d need an opaque constructor function for components to trigger the value restriction.
  • Monomorphic staging through some Applicative. That is, there’s an enforced phase distinction between a polymorphic component description and it’s monomorphic representation at runtime. Monad is potentially unnecessary, but it’s what we are used to. So in the case of type Component p = Effect (p -> JSX), you can have forall a. Effect (a -> JSX) but never Effect (forall a. a -> JSX). The polymorphism can only occur on the component description, not it’s runtime representation. This is enforcing the value restriction monadically.

I don’t think there’s a need for createElement to be in Effect. createElement itself isn’t at the intersection of those three things. If component instantiation is staged monadically, then createElement is only ever dealing with monotypes, and thus doesn’t let you observe polymorphism/mutation itself.

1 Like

To clarify this, the monadic staging has to create a unique reference for each type instantiation. If it exposes a shared reference for different type instantiations, it’s still unsound.

counter example in js

codesandbox

import "./styles.css";
import React, { useState } from 'react';


const Counter = (props) => {
  const [state, setState] = useState(props.val);

  const handleClick = () => setState(props.set);

  return (
    <button onClick={handleClick}>
      <span>{props.get(state)}</span>
    </button>
  );
};


function App1() {
  const [flag, setFlag] = useState(true);

  return (
    <div>
      <button onClick={() => setFlag(!flag)}>Toggle type {flag.toString()}</button>
      {flag ? (
        <Counter val={0} set={(x) => x + 2} get={(x) => x} />
      ) : (
        <Counter val={0} set={(x) => x + 1} get={(x) => x} />
      )}
    </div>
  );
}
// 2 4 click 5 6

function App2() {
  const [flag, setFlag] = useState(true);

  return (
    <div>
      <button onClick={() => setFlag(!flag)}>Toggle type {flag.toString()}</button>
      {flag ? (
        <Counter val={0} set={(x) => x + 1} get={(x) => x} />
      ) : (
        <Counter val="0" set={(x) => x + "1"} get={(x) => x} />
      )}
    </div>
  );
}
// 1 2 click "21" "211" click "2111" "21111"


export default function App() {
  return (
    <>
      <App1/>
      <App2/>
    </>
  )
}

Understood @natefaubion, thanks for explaining.

So we are using monadic staging to access context (created with createContext of react).

But monadic staging in context case is restricting global access of context’s reference, so reference can only be passed down with prop drilling (which is defeating the purpose of context).

Is there a way to solve “value restriction” without monadic staging, or is there a way to still access context reference globally after monadic staging?

Or is there a way to restrict creating polymorphic context ?

Although global access can be achieved using unsafePerformEffect. As you mentioned above Monomorphic staging we are achieving through Applicative. I’m curious to know if there is any other way to achieve Monomorphic staging.

By ‘globally accessible,’ I mean that it should be accessible to any function without needing to be passed as an argument.

There is no typesafe way to use global context in PureScript, unfortunately. It fundamentally relies on top-level referential identity for ergonomics which is just not a thing in PureScript semantics.

I don’t think you need prop drilling per se. You can use ReaderT for staging instead of just Effect. So instead of:

type Component props = Effect (props -> JSX)

you can do

type ComponentEnv e props = ReaderT e Effect (props -> JSX)

Then you can do:

myComponent :: forall r. ComponentEnv { myContext :: Context String | r } MyProps
myComponent = ReaderT do
  { myContext } <- ask
  lift $ component "MyComponent" \props -> ...

So it’s passed around at the staging layer, not in props. This still has advantages. The context is initialized at staging time, but the values can still be dynamically accessed at runtime.

Although ReaderT should solve the issue of prop drilling, I have some shared projects where I have some components and contexts, so If I have to pass context via ReaderT, I’ll have to maintain a shared type across projects. And I’ll try to evaluate overhead of ReaderT binds.

Thanks for the help @natefaubion.

What type do you have to share? With extensible records you don’t have to share anything you don’t want to share. You can declare only the dependencies you use.

Any overhead only happens on app start. The outer Effect layer of Component doesn’t happen as part your app runtime. The overhead could be eliminated with purs-backend-es and the optimizer, so all your left with is an extra argument at app start. I think that’s pretty minimal?

1 Like

cool, yeah, I can use extensible records for my case. Need to modify my current code which has context creation done with unsafePerformEffect :smiling_face_with_tear:. and compared the output of purescript-backend-es with regular spago build. my application is targeting minimum chrome version 37, which does not support ES6. so I have to use babel to convert it to ES5 which I think will minimize the optimisations of backend-es.
Thanks.