The one sentence summary I would use for algebraic effects is “they’re the way to add delimited continuations to a statically typed language.” Similarly, one might say that ML/ocaml/haskell style polymorphism [excluding typeclasses] is the way to take functions like map or filter from dynamically typed to statically typed languages.
I guess delimited continuations are an ok model for how react hooks work except that (as far as I’m aware) nothing continuation-like is involved. Instead one might think of them as scheduling work to be done very soon (in some sense) after your function runs.
An alternative question is: how do you type something like hooks? I think this mostly boils down to: how do you enforce the rules of hook usage with types? The rules are that: 1. Only certain functions may use hooks, 2. Any function which uses hooks must always create hooks (including hooks created by called functions) in exactly the same order every time it is run.
Algebraic effects aren’t really suitable for enforcing this rule (the OP has an example of state effects but there is only one state. It misses the killer feature of hooks which is to say in a relatively ad-hoc way, “I need a bit of state, please let me use some”. Instead you need to add an effect and handler for every bit of state or thread through access to a single bit of state (or have code where one must manually ensure that the rules are followed, eg a “useState” effect)).
I tried writing such a thing in ocaml about a year ago. I used a monadic interface with a second type parameter tracking the types of the states which are created. But this isn’t sufficient as you can still write rule-breaking code like:
function
| true -> map (useState 1) ...
| false -> map (useState 2) ...
You need to ensure that the type parameter tracking the calls is always abstract in such a way that the compiler will never determine that it is equal to a different useState. But I didn’t think of a way to do that and stopped looking.