Haxe-react, functional components and hooks

Hi guys!

Here’s my take on using a Haxe/React functional component with hooks (useState, useRef, useEffect) with react 16.8.

What do you think about it? Are there better ways of destructuring the return array of useState()…?
(As Haxe unfortunately doesn’t have object/array destructuring yet (Destructuring assignments · Issue #4300 · HaxeFoundation/haxe · GitHub), I “destructure” the return array [state, setState] into an object for each value via an temp Array - not very elegant. How would you guys do this? Macros, I guess…)

EDIT: Using untyped for the hooks here - someone’s having externs for them?

/ Jonas

import react.ReactMacro.jsx;

class App {
	static public function main() {
		var body = js.Browser.document.querySelector('main');
		react.ReactDOM.render(jsx('<TestHooks defaultText=${'Abc123'} />'), body);
	}

	static public function TestHooks(props:{defaultText:String}):react.ReactComponent.ReactElement {
		//----------------------------------------------
		// State hooks
		var temp:Array<Dynamic> = untyped React.useState(props.defaultText);
		var text:{state:String, setState:Dynamic->String} = {state: temp[0], setState: temp[1]};

		var temp:Array<Dynamic> = untyped React.useState(222);
		var num:{state:Int, setState:Dynamic->Int} = {state: temp[0], setState: temp[1]};

		//----------------------------------------------
		// Ref hooks
		
		// We must use ref for render counter - it can't be held in state, because updating a state counter
		// would cause another render, causing an inifite loop
		var renderCountRef:{current:Int} = untyped React.useRef(1);

		var inputRef:{current:js.html.InputElement} = untyped React.useRef();

		//---------------------------------------
		// Effect hooks
		var alwaysEffect:(f:Void->Void)->Void = untyped React.useEffect;
		alwaysEffect(() -> {
			trace('run on each render');
			renderCountRef.current++; // increase counter on each render
		});

		var numEffect:(f:Void->Void, a:Array<Int>)->Void = untyped React.useEffect;
		numEffect(() -> {
			trace('run when num is changed, but not when text is changed');
		}, [num.state]);

		//------------------------------------------------
		// Render
		return jsx('<div>
			<h3>Haxe, React, Functional components, Hooks</h3>			
			<div>text: <input key="input" ref=${inputRef} value=${text.state} onChange=${e -> text.setState(e.target.value)} />	</div>
			<div>
				<button onClick=${e -> num.setState(() -> num.state + 1)}>Inc num</button>		
				<button onClick=${e -> num.setState(() -> num.state - 1)}>Dec num</button>	
				<span>num: ${num.state}</span>
			</div>
			<p>Render count: ${renderCountRef.current} </p>
			<button onClick=${e -> inputRef.current.focus()}>Set input field focus</button>
		</div>');
	}
}

Well, long story short:

  • haxe-react isn’t rushing to support them, because AFAIK MassiveInteractive is kinda sunsetting Haxe and so the lib is used only in pretty old projects with pretty old react versions
  • haxe-react-next probably won’t be adding them because Rudy hates hooks and I can’t say I blame him

I don’t really want to bore you with my opinions on all the things that are wrong with hooks, so let’s get straight to how one would use them:

@:native('React')
extern class Hooks {
  static function useRef<T>(?value:T):{ current:T };
  static function useState<T>(init:T):HookState<T>;
  static function useEffect(fx:()->Void, ?dependencies:Array<Dynamic>):Void;
}

abstract HookState<T>(Array<Dynamic>) {
  public var value(get, set):T;
    function get_value():T
      return this[0];
    function set_value(param:T):T {
      this[0] = param;// may not be necessary, but in concurrent mode this should lead to more consistent state - that's why JS react users are compelled to use setState(prev => prev + 1)
      this[1](param);
      return param;
    }
}

The general rule is: if you have something in JS that doesn’t map onto the Haxe type system, make an abstract over untyped code.

Which would then make your code look like so:

import react.ReactMacro.jsx;

class App {
	static public function main() {
		var body = js.Browser.document.querySelector('main');
		react.ReactDOM.render(jsx('<TestHooks defaultText=${'Abc123'} />'), body);
	}

	static public function TestHooks(props:{defaultText:String}):react.ReactComponent.ReactElement {
		//----------------------------------------------
    // State hooks
    var text = Hooks.useState(props.defaultText);

		var num = Hooks.useState(222);

		//----------------------------------------------
		// Ref hooks

		// We must use ref for render counter - it can't be held in state, because updating a state counter
		// would cause another render, causing an inifite loop
		var renderCountRef = Hooks.useRef(1);

		var inputRef = Hooks.useRef();

		//---------------------------------------
		// Effect hooks

		Hooks.useEffect(() -> {
			trace('run on each render');
			renderCountRef.current++; // increase counter on each render
		});

		Hooks.useEffect(() -> {
			trace('run when num is changed, but not when text is changed');
		}, [num.value]);

		//------------------------------------------------
		// Render
		return jsx('<div>
			<h3>Haxe, React, Functional components, Hooks</h3>
			<div>text: <input key="input" ref=${inputRef} value=${text.value} onChange=${e -> text.value = e.target.value} />	</div>
			<div>
				<button onClick=${e -> num.value +=1}>Inc num</button>
				<button onClick=${e -> num.value -=1}>Dec num</button>
				<span>num: ${num.value}</span>
			</div>
			<p>Render count: ${renderCountRef.current} </p>
			<button onClick=${e -> inputRef.current.focus()}>Set input field focus</button>
		</div>');
	}
}
6 Likes

Thank you, Juraj, for your elaborate answer and elegant solution! :slight_smile:

Good or bad, React with functional components and hooks are everywhere, and when dealing with react code and projects, it’s so much won when you’re able to “map” js workflow to haxe without too much reinterpretation.

Very elegant to map the useState return array to a getter/setter pair…! Thanks again!

/ Jonas

1 Like

This is such an obvious solution to these issues that I never thought of, wow. I feel really silly :sweat_smile:

Thanks!

2 Likes

but this useState return 2 of vaule , how to get setChecked as below?

const [checked, setChecked] = React.useState(true);

@Zhan: In Juraj´s solution, the javascript way of handling it (with [checked, setChecked] for value and setter method respectively) is hidden behind the HookState abstract. You can ineract with it as following:

Get value:
var isChecked = checked.value;

Set the value:
checked.value = true;

Here, you have a video exemple showing just this:

Have a look at 11´30.

thank you for your video.

and current I use react-next instead of haxe-react .

and have you try use react-next ?

I’ll check you video ,thanks again.

Juraj’s solution should work with react-next too

1 Like