Closed T1mL3arn closed 2 years ago
Well, this is not really unexpected. Even in react, if you mount a functional component directly, it will not update when external state it depends on changes. It only rerenders if:
If you just go ReactDOM.render(<FunctionComponent count={something} />, container))
the component is not going to rerender if something
changes, unless for example something
is backed by mobx and you made FunctionComponent
an observer, in which case it is no longer a pure functional component.
Coconut does not even have functional components per se (at least for now). Functions are simply called and the returned vdom is processed as if you had written it inline.
However, you can put arbitrary subscription/update cycles into any part of the vdom with coconut.ui.Isolated
, if you want.
Renderer.mount(js.Browser.document.getElementById('function'), <Isolated><FunctionView count={count} /><Isolated>);
It's primarily meant to be used within big views to avoid updating the whole DOM when a small part changes, as shown in this simplistic example:
class Counter extends View {
@:state var count = 0;
var rendered = 0;
function render()
return <button onclick={count++}>(rendered {rendered++} times) <Isolated>counter: {count}</Isolated></button>;
}
If you try that, you'll see that the Counter
view itself never needs to rerender - although when removing the Isolated
, the change affects the whole view.
Personally, I would advise against using anything like react portals. So far I've failed to find a use case that isn't better covered by other methods. The example given in the doc are modals, which I would propose implementing via implicits. Among other things that'll allow non-global management of modals (imagine your application has its visual area split in two and each should be allowed to its own modal) and makes testing easier to (the portals based implementation relies on document.getElementById('modal-root');
actually returning something).
Here's a contrived implementation to illustrate the general approach:
import coconut.ui.*;
import coconut.Ui.hxx;
import js.Browser.*;
function main() {
Renderer.mount(
document.body,
<div style={{ height: '100%', width: '100%', textAlign: 'center' }}>
<div style={{ height: '40%'}}>
<Modals>
<Opener />
</Modals>
</div>
<Opener />
<div style={{ height: '40%'}}>
<Modals>
<Opener />
</Modals>
</div>
</div>
);
}
class Opener extends View {
@:implicit var modals:Modals;
function render()
return <button onclick={modals.open(<Modal onclose={modals.close()} />)}>Open</button>;
}
class Modal extends View {
@:attribute function onclose() {}
function render()
return <div style={{ background: 'white', width: '50%', height: '50%', position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%, -50%)', padding: '20px', boxSizing: 'borderBox', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', alignItems: 'center' }}>
Look at me, I am a modal!
<button onclick=${onclose}>Close</button>
</div>
;
}
@:default(Modals.DEFAULT)
class Modals extends View {
static var counter = 0;
public final id = counter++;
@:children var content:Children = null;
@:state var current:haxe.ds.Option<RenderResult> = None;
public function open(v)
current = Some(v);
public function close()
current = None;
function render()
return <div style={{ width: '100%', height: '100%', position: 'relative' }}>
<Implicit defaults={[Modals => this]}>{...content}</Implicit>
<switch {current}>
<case {Some(v)}>
<div style={{ position: 'absolute', top: '0', left: '0', right: '0', bottom: '0', background: 'rgba(0,0,0,.25)', pointerEvents: 'auto' }}>{v}</div>
<case {_}>
</switch>
</div>
;
static public var DEFAULT(get, null):Modals;
static function get_DEFAULT()
return switch DEFAULT {
case null:
var container = document.createDivElement();
container.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none';
document.body.appendChild(container);
Renderer.mount(container, <Modals ref={v -> DEFAULT = v}/>);
DEFAULT;
case v: v;
}
}
Thanks for such detailed explanation and modal example. Though it is not quite for my case.
Sometimes you have to escape your views layout to avoid styling problem. For example, I am making a userscript that adds views to different places in existing markup (those views include modals as well as other elements). I see no way to do this otherwise than
// <App /> viewDidMount():
Renderer.mount(targetA, <OpenModal />);
Renderer.mount(targetB, <Modal show={state.showModal} />);
Renderer.mount(targetC, <AnotherThing />);
(then I found this issue subject (when my views was functions they did not work) but it appeared to be designed that way and I can fix it anyway using class views)
I could try to patch site's styles to fit my needs, but I believe it is not a good solution (and might be cumbersome), so I tried to craft portal analogue which allows to do things as simple as this:
// <App /> render():
return (
<div>
<AdminPanel />
<if {state.showDialog== true}>
<Portal target={document.body}>
<Dialog text={state.dialogText} />
</Portal>
</if>
<Portal target={somewhereInFooter}>
<Foo value={state.fooValue}>
<Bar value={state.barValue} />
</Foo>
<Bazz />
</Portal>
</div>
);
And this mostly works for simple static layout inside portals (and sometimes for dynamic layout). Though in some cases when such portals get unmounted the app crashes (but this is a subject for separate issue-question).
Sometimes you have to escape your views layout to avoid styling problem.
In the example I gave the Opener
that's not nested in Modals
makes its modal render completely outside its own view hierarchy. Nothing about the approach forces you render anything into any other particular thing.
The point is that instead of arbitrarily and directly rendering into other parts of the DOM that may or may not be there and that you hardcode in unrelated parts of the view hierarchy, you operate on an implicitly passed entity (the cleanest implementation would pass around a narrow interface rather than a full blown view), for which you have the guarantee that it'll always be there. Among other things, that's easier to test.
The code that you're looking to write depends on both global DOM (document.body
) and internal structure of other components (somewhereInFooter
). I advise against both ;)
Trying to implement something similar to react portals using
Renderer.mount()
andviewDidMount()
I found some unexpected(?) behavior: function view does not update when parent state changesother files
hxml ```hxml -cp src -main TestBugMain --library coconut.ui --library coconut.vdom -dce full -D analyzer-optimize -D js-es=6 -js bug\bundle.js ``` Main ```haxe class TestBugMain { static public function main() { Renderer.mount(js.Browser.document.body,Clicking the button updates only class view, while I expect updating function component as well.