I started learning React for starting a large project a few months ago. I have not really liked the experience very much. One seems to still need to know all the JS/CSS/HTML I've been using for 10+ years but also a new stack on top of it, simultaneously.
Hooks are one of the strangest features I've seen in a language yet. I doubt they will be a lasting paradigm outside of the React ecosystem.
I like JSX a lot. The idea of reusable stateless components is totally on the right track.
I've found React to be very hard to debug. I get stack traces that don't seem to start from any code I've written.
I now somehow suspect that the next wave in the JS ecosystem will be doing something like microservices on the frontend - message passing between isolated webworkers? I ponder this because React has not, for me, achieved any level of enforcing a sane way of deciding which objects in memory belong to a given component. Something feels really wrong with the the React model of programming.
Writing a project in React seems doubly frustrating - there's the challenge of a new stack and new programming model, but I don't seem to be getting anything for the trade: the code isn't easily debuggable, doesn't have a great upgrade and maintainability story, the app still has race conditions, memory leaks, browser incompatibilities, tightly coupled components. The problem seems to be that React only gently encourages, but does not enforce, any good practices.
I too can't shake the feeling that hooks are off. I like JSX and the component model.
In React and like-minded projects, I look at a stack trace and see that it starts at some kind of batch renderer. I can't tell "why", "how" or sometimes even "what" broke. The input part of it is completely lost.
I work on a Backbone application where stack traces are much more obvious. They usually tell you the whole story. I find that juniors who were mostly exposed to React have a tendency to completely ignore stack traces, and am now wondering if they are just conditioned to them being unhelpful.
I worked on a Backbone app for several years (and still do, technically - we're about to do a final push to finish migrating it to React).
I also liked the fact that you could step into Backbone code, and back out to app code on the other side. Backbone's source is small, and you can see exactly what it's doing.
However... React is simply a fundamentally superior programming model, for a wide variety of reasons. I can treat React as a black box, knowing that if there's a problem in my UI, it's because of either the logic in my components, or the data those components are using for display. (Then there's things like being able to arbitrarily compose child components together and pass them props, vs randomly trying to attach subviews or something via umpteen different plugins.)
I'll agree that seeing a stack trace with React library code isn't typically helpful. But, the error messages are usually sufficient to tell me what kind of an issue is going on, and point me to the right component or part of the tree to start debugging the real issue.
Somewhat related to this, I put together an extensive post earlier this year called "A (Mostly) Complete Guide to React Rendering Behavior" [0] that digs into detail on how React's rendering actually works, as I've found that a lot of people seemed to be missing how some of the pieces of rendering fit together:
One thing that feels off about hooks to me is the "primitive" ones React provides. Like, here are the basic things that hooks can do:
1. Tell the component to re-render
2. Store an object that persists between component renders
3. Run something after the component has been rendered
4. Run something when the component unmounts
React doesn't give you functions that do these operations individually. Instead, you get weird hybrids like useState, which combines storing a value with telling the component to rerender when the value changes. So you sometimes have to jump through hoops to get the behavior you want. (Well, useRef sort of does (2), except it constructs the object itself.)
Components are functions of their props and state, returning markup (in the form of a descriptor foe the rendering engine). When a prop or piece of state changes, the component must re-render. The fact that useState might be responsible isn’t relevant.
That said, I’m also still struggling with hooks (and I like functional programming in JS). I want to like that hooks let you wrap your domain logic into neat bundles, but I find that I miss the lifecycle callbacks of the older class based style.
Hooks based components are trying to make modules that are “about” the problem domain they illustrate, but they do it as a feature of React. React isn’t about your problem domain, it’s about driving the DOM and rendering, and I think it makes more sense to treat React code that way.
I’m open to having my mind changed on this. I have spent way more time writing classical React than hooks-based.
> Hooks based components are trying to make modules that are “about” the problem domain they illustrate, but they do it as a feature of React. React isn’t about your problem domain, it’s about driving the DOM and rendering, and I think it makes more sense to treat React code that way.
This is why I think it's going to take a bit more time with hooks code to realize the opposite to csande17's complaint is really true: the problem with the hooks React ships with is not that they aren't primitive enough (as the sibling comment points out, csande17 probably is also missing that some of the primitives they are looking for expressly don't exist as React hooks because they don't fit the algebraic effect model React is moving toward, versus the lifecycle model React is moving away from), but instead that the hooks React ships with are still (or least feel) far too primitive an algebra for the "calculus" needed for at least some problem domains (if not most problem domains).
I don't specifically have any answers for what the next layers up will be, though I appreciate this article for trying to explain how algebraic effects work in other languages because that provides ideas from other places of how their primitives get composed.
> but I find that I miss the lifecycle callbacks of the older class based style.
Why not continue to use the class based style? It's still supported. We're using hooks for some simpler cases where they really cut down on boilerplate, but we're continuing to use classes for more complex cases where we find it more readable.
That's what I did at my last gig, where I owned the codebase. At my current gig, I share it with 10 people and the decision to move to hooks was made before I joined.
I don't think hooks can actually do 1, 3 and 4. Components re-render whenever React feels like it's a good moment to do so. That happens to include "when state changes", but you're not able to rely on that. This will supposedly become more apparent once Concurrent Mode finally lands.
(You can certainly fault React for it being hard to grok the "correct" mental model, but it makes sense that its primitives do not match the wrong mental model to me.)
> I too can't shake the feeling that hooks are off.
Well the alternative to hooks is componentDidMount, componentDidUpdate, etc. It's a lot easier to sync state and UI when the logic for doing so can be put in one place.
I’ve been using hooks for a year, and they are awesome (once you get used to it). I can write the same component in 1/3 of the code in a functional style.
The debugging story isn’t great if you expect to be able to use breakpoints.
It’s all about putting console.log wherever really.
The beauty comes in the speed of coding, I can move so fast that I always build a mock version of my api layer and can have the entire UI done rapidly.
+1 with the stack traces; I long for simple and straightforward, no magic applications.
The front-end ecosystem seems to move to the opposite; multi-stage compilation / transpilation steps even in development mode, with tooling to make it manageable and debuggable (source maps). The development mode of my current application needs to reload 35MB worth of stuff at every refresh.
(should look into whether I can optimize that, idk).
The value of hooks became apparent to me after working with a very large react-redux application. It was so large and the forms had so many elements that the capture of data, testing, and subsequent state management in redux was enormous.
It wasn't overly complicated from a technical perspective, but when there were forms with say 50-100 different inputs and dozens of state transitions, it was a mentally taxing experience every time to try to maintain the data model and state machine in my head when debugging or introducing changes.
For me the introduction of hooks was amazing as it's allowed us to strip most of the redux state management in favour of managing state with hooks inside of functional components. We still use redux for global application state, but there are also hooks for interacting with that redux state too.
We effectively went from class-based components in react-redux with say 500 LOC in each class, 500 LOC in each action file, 500 LOC in each reducer file, all the way down to about 700 LOC in each functional component file.
I do agree though that debugging sucks. It's incrementally getting better, but it's got a long way to go.
FWIW, you should also switch to using our official Redux Toolkit package [0] if you haven't already. RTK is our recommended approach to writing Redux logic. It includes a number of utilities that build in our best practices, prevent common mistakes, and simplify most Redux use cases.
Related to that, we've rewritten the Redux docs tutorials to teach use of Redux Toolkit and the React-Redux hooks API as the default, as well as using simpler patterns like structuring Redux logic as single-file "slices". See the new "Redux Essentials" [1] and "Redux Fundamentals" tutorials [2] for examples.
While I totally agree that hooks reduce code size a lot, and have used them to great effect, the feeling that hooks are weird has not faded.
I know exactly what they do, and usually help co-workers with weird edge cases, but I still have a feeling that we could do better.
Anyway wanted to mention that since redux is so ridiculously tiny, its simple to actually nest it. Components can have local state, and only transmit global state on business related changes.
Now with hooks and useState thats a bit more ergonomic, but you could do that way before hooks were introduced.
I know what you mean to some extent. I feel hooks are very powerful but I think they are only a refinement on a fundamental problem which is handling reactivity in your view layer is always going to have issues. I would guess you kind of hit on something similar by having your localized Redux states.
Something I'm looking at right now is Effector. It encourages many smaller stores and allows for handling of effects quite easily. The smaller individual stores avoid the performance issues that people bring up often with Redux and I feel the API is more fluent, avoiding some of those boilerplate concerns, some of which are admittedly addressed by Redux Toolkit.
I'm not sure how my time with Effector is going to play out but I suspect it is at least the right idea. I wouldn't be surprised if a similar library came out that was embraced more by the community
> For me the introduction of hooks was amazing as it's allowed us to strip most of the redux state management in favour of managing state with hooks inside of functional components. We still use redux for global application state, but there are also hooks for interacting with that redux state too.
I stopped writing React around the time Hooks came out, but I used to use a similar pattern with higher order components. In particular, I preferred HOC for forms. From what you're saying it sounds like hooks replaced a lot of that HOC functionality. Are HOC still used at all?
HOC have mostly fallen out of favour due to hooks. Typing hooks is a little easier than using HOC because you don't have to determine the type of your props which could conceivably be a combination of multiple HOC and the actual props your component expects. With hooks your props are your props which you have a simple interface for and then the hooks can be typed based on their parameters and what they return like any other function.
HOC are the more lispy/dynamic way of doing it I would say and in Clojurescript using Reagent it has always been common to use that general pattern I feel.
But now in 2020 you have 2 reasons to pick React. One is because it's the right tool. The second is to do with the JS tech battlefield - you pick React because it won the front-end popularity fight, and is probably safe for the next 5 years, and you can hire React developers, and you can hire them knowing they will want to stay React developers etc. Yes all that BS!
Yeah sure. You choose the right tool because it it the right tool.
>"The second is to do with the JS tech battlefield - you pick React because it won the front-end popularity fight"
If we always pick "most popular" there will be only one left.
>"you can hire React developers, and you can hire them knowing they will want to stay React developers etc."
I guess some big companies might hire narrow scope developers (and throw those away when they the size of tool they were using changed from 9mm to 10mm). Me being more down to Earth I prefer to look at the resume and list of accomplished projects and hire persons who are creative and do not get fixated on a single thing.
>"Also you might pick React for your own CV!"
Sorry, no ;) I pick projects for money and being interesting. Using XYZ language/framework does not come as a reason into that picture for me.
I like operating in niche markets. So far (fingers crossed) it pays my bills and frankly being independent I do not worry too much about common trends. Only what concerns me directly. Not that I am being ignorant, I constantly read stuff/play with it but as long as I can afford it I prefer to go the way I find suitable from my perspective. Do not give a hoot about some abstract everybody does this/that spoonfed by big companies.
Something is off with your transpiling set up if you’re not getting accurate stack traces. A “create react app” default setup will should have accurate stack traces out of the box.
To your point about best practices React, at least at the beginning was not supposed to be the entire stack, by design you needed other libraries to deal with large portions of the web state and as such it could only control a small part of your stack.
That being said with hooks and providers, it is gaining enough internal tooling to be almost “the whole stack” but still the philosophy remains.
What GP means is that the stack traces will often just point you to React’s runtime code.
This is a general debugability problem that all runtimes have-you now need an understanding of how React’s implementation works to map the runtime’s state to what you’re attempting to do.
> What GP means is that the stack traces will often just point you to React’s runtime code.
I very rarely run into a case where that occurs, and almost never that doesn't involve ignoring one of the ESLint warnings that CRA sets up in its default settings.
Now, getting the stack trace to only point you to some part of a component library's code seems more common.
I run into this daily. There isn’t a single JS error in our monitoring that has a useful stack trace. It would have to extend another 30 layers down to start going into actual application components or code. They all look the same going through different bits of React internals.
I click on my errors and they take me to the exact line of code that caused them.... basically the JS error throws the line number and file path and then my terminal setup makes it clickable.
Usually it’s always the same error anyways. Something is undefined or typed wrong.
I click on my errors and they take me to the exact line of code that caused them.... basically the JS error throws the line number and file path and then my terminal setup makes it clickable.
This highlights one of the biggest issues with React: usually it works really well, but if you're unlucky enough that it doesn't then there's almost no way to work out why. I've been developing with React for about 4 years and I just don't upgrade things in the ecosystem unless I absolutely have to, because debugging the tooling is a massive pain.
This is also why it's very unhelpful to say that it works. Yes, it usually does, but not always and failing to understand that comes across as somewhat antagonistic.
I completely understand what you mean about not upgrading things in the working ecosystem, it's the pragmatic choice in the short term.
But then the churn in the interdependent platform and packages means your project is moving towards a state that it cannot be upgraded without significant risk and rewriting around the deprecated packages.
In my opinion this is a major issue with React in the landscape of businesses that have a halflife of more than a few years.. ie most long term businesses.
Yet despite the long term fragility and maintenance issues it's a topic that is missing in most discussions about React. The React conversations tend to be 'how can we keep up with the latest churn' instead of recognising the churn itself is a problem and stability provides realworld business value in the medium and long term.
That’s sourcemaps. For simple errors like you mentioned it might be enough, but your stack is still useless for more complex issues. Usually it’s dominated by hooks / batch rendering code.
Hooks have unlocked a power in React and front end development we haven't seen before, that is going to lead to bigger things. I don't know if hooks are going to be the end state, but the ability to create reusable functionality encapsulated in a hook has solved a lot of problems we previously had of the awkwardness of sharing behavior across components.
Reusable encapsulated functionality for front end components has existed before in a number of front end frameworks, I can cite Delphi VCL and .NET Winforms as two I'm familiar with. Have a look at how tooltips are implemented in Winforms ..
I've stated several times that hooks are awful, and the general response I get is "oh, you just don't get it," but the way they are implemented is not very functional, but I guess the point is that they are side effects.
In terms of other things, JSX has nothing to do with stateless components, it's just a way to simplify writing `React.createElement(yourElementOrTagName, { ...props }, [ ...children ]);`
And for debugging, the absolute best advice I can give is to build smaller components, and use something like storybook to demo/isolate behaviour and tinker with it.
In general, react is a perfectly fine choice, though. I don't think it's any more strange than angular/vue/hyperapp/etc., but it is certainly lower level, which can feel less intuitive.
Agree about the hard to debug problem with React.
Especially in SPA routing, normal navigation works fine, but in some cases, SPA navigation breaks the new route just because SPA routing is not stateless somehow.
I like JSX a lot. The idea of reusable stateless components is totally on the right track.
The latter is what I really liked about the early versions of React, back when it was seen as “the V in MVC”. The basic premise of having a declarative UI, so you only had to define what is rendered in terms of absolute state rather than relative transitions, was a good one. React’s virtual DOM mechanics made working that way reasonably efficient for most practical purposes. The rapid adoption in those early years is testament to how useful that combination of declarative+efficient really was.
However, I feel like as time has passed, the emphasis in the community has shifted towards trying to shove more and more that isn’t view logic into the React/JSX parts of their code, and that has caused all kinds of unnecessary problems.
At first it was as if, just because we had a tidy mechanism to write nested components easily using JSX, every little aspect of application behaviour was now supposed to be implemented as a component and shoved into the tree. This led to patterns like higher order components, and thence to more patterns trying to manage the added complexity introduced by having lots of non-rendering components wrapping everything. By this time, the inmates had taken over the asylum and any pretence of React being used primarily as a clean, declarative UI tool had been all but forgotten.
The idea of managing application state separately and systematically was old long before React came along. Relational databases and ACID transactions had been a cornerstone of software architecture for decades by then, as had the idea of having a separate data model to hold and systematically update application state within desktop software. The early React community was clearly aware of these ideas as well, not just from the “V in MVC” description but also the idea of Flux and before long the Redux ecosystem. And yet, developers still often seemed to keep their application state within their React components instead, which predictably led to all the same problems that had once caused traditional GUIs to move away from retained mode and towards separate state management and immediate mode rendering in the first place. This in turn has led to interest from the React community in ideas like context, and today along with hooks we often see talk of replacing tools like Redux with React Context instead.
It’s not just application state, either. We’ve seen more and more responsibilities pushed into React components over time. It doesn’t seem unusual to have those React components even dealing with communications to transfer state between the browser and the server now, along with all of the awkward mechanics like async fetching from APIs and cache management.
Most recently, the talk is all about how great hooks are, because they let you consolidate or remove of lots of awkward logic that used to be spread through different lifecycle methods in class components. Sometimes, as with this article, the idea gets taken to an extreme. But I can’t help wondering how often anyone stops to ask whether 99% of that logic should ever have been tangled up with the rendering code of the application in the first place. Aren’t we in danger of regressing to big-ball-of-mud antipatterns, just using a big-tree-of-components instead this time around? How do you even test an application properly at scale if you’re forever tangling up your rendering logic, state management, client-server communications and who knows what else, and then obscuring much of it inside React components that end up being tightly coupled because they rely on a certain hierarchy to set everything up just right?
React itself still seems like a useful tool for the same reasons as always, and functional components are a nice incremental improvement on the boilerplate that came with class components in the early days. But the grumpy old man in me can’t help feeling that a lot of the other “progress” in the React ecosystem has been more a series of missteps taken because those who don’t study history are doomed to repeat it. There is still a lot to be said today for using React as a nice, clean, reasonably efficient rendering library for your UI, and for keeping other concerns like state management and network communications separate and using the right tools for each job.
I think a lot of what you're saying is very true. Its been said before that the React and SPA community in general has had the pleasure of rediscovering past concepts as the community has evolved. I suspect in a couple of years handling application state in your view layer, even using their native capabilities like Context in React or Vuex in Vue will be viewed as an anti-pattern.
As I said in another comment. Effector is something that interests me as it seems to hit a lot of right ideas in terms of reactivity outside the view layer. It can hook into React with hooks or Vue with the Composition API. Hell its stores just work natively with Svelte.
This brings us onto the most interesting point. Virtual DOM libraries aren't necessarily the only/best solution. Compiled solutions like Svelte and SolidJS are offering alternatives that address some of the size and performance concerns that VDOM libraries raise. If your state management is tied into that whole ecosystem, you have to throw all that away to benefit for advancements in the view layer. This is to say nothing of the fact that even sharing code between React and React Native is difficult to do easily.
Whether Effector is the answer I don't know, but I believe it has the right ideas (it is inspired by the likes of Reframe and Flux). Something else might come along that is similar but presented better. However I could totally see a movement that espouses developing your apps data first and then wiring in your UI layer later. Maybe it would even have a TDD or repl based approach. Unfortunately I think it would ultimately be at odds with the beginner friendly, just get something working perspective that a lot of libraries take. How to bring new starters along so they can follow and understand is a much bigger question
This brings us onto the most interesting point. Virtual DOM libraries aren't necessarily the only/best solution.
That is surely true as well. Even a perfect implementation of a VDOM library is necessarily doing a lot of extra work compared to direct DOM updates if you already know exactly what needs changing. For a “typical” SPA, if there is such a thing, React’s virtual DOM system is fast enough to make its declarative UI strategy viable, which is enough to make it widely useful. However, it can still become a bottleneck given sufficiently demanding conditions. React without a lot of help can still choke quite easily if it’s asked to render something like a table with thousands of cells or an intricate SVG diagram that needs to animate smoothly.
Giving React the help it needs typically means bypassing some of the diff logic one way or another, for example by providing sCU in a class-based component or a list of dependencies to hooks. However, these kinds of changes also introduce potential inconsistency between what you’d normally render and what gets rendered with a bypass in place, which creates an opportunity for bugs and seems to be fighting against the whole idea of declaring your UI once and then letting React worry about all the details of maintaining it.
Hooks have a very nice synergy with Typescript. useEffect can be tricky to get right in some cases but overall a prefer writing in pure components with hooks over classes.
Just a slight nitpick about components with hooks. They are neither pure nor functional. They are function components. We can still have pure components without hooks but function components are no longer pure by default. Its actually one of the downsides of the move to hooks. It used to be very obvious when you needed to worry about state and when you didn't. However the benefits of hooks outweigh that nicety in my opinion
Best way to debug react is to do it via your IDE debug functionality, to enable debugger on caught errors (if you get errors in other code you usually can blackbox it) or to utilize `debugger;` command. Don't forget browser extension for cases where no error happens.
You can set breakpoint inside useEffect and it will work as expected, you can use something like
if (somevar === 42) debugger;
and you'll see all of the internal call stack and all variables in scope. moreover you can use the console to evaluate your code. Worth mentioning 99% of strange issues in react (in my experience) are caused by mutation or not declaring all deps in hooks.
I've read a bit about algebraic effects in the past (independent of React), but I think they are much harder to grok than hooks. I tried many times but I never really got it. Like Haskell has this way of making you think you've got it for a split second, then you try to do something and you realise you don't have a clue. The effect systems using free monads seem to keep applying a functor to itself an unlimited amount of times and I really didn't get it. I even did that category theory youtube course for programmers. Still don't get it. I could probably get it if I went full time on just that for 2 months or so, then coded in that style for a year to really bed it in.
Anyway...
However I don't find hooks that hard to understand. I don't think you need to touch algebraic effects to get it. It's yet another view system (like having done windows forms, wpf, asp.net webforms, mvc, knockout js, RX, etc.) to get your head around how it works.
In this case you have a function that returns the desired state and with useEffect you can set up actions to be run after that view gets rendered and added to the DOM, with some conditions on which renders you are interested in (all of them? just when foo changes?). And another callback for when you are "done" so you can clean up.
> The effect systems using free monads seem to keep applying a functor to itself an unlimited amount of times and I really didn't get it.
I can't help with the "effect systems" side of this, but I might be able to elucidate some of the recursive functor thing for you.
Suppose you've got a binary tree structure. In something like Haskell, you might model it like so:
data BinaryTree t where
InternalNode :: (BinaryTree t, BinaryTree t) -> BinaryTree t
LeafNode :: t -> BinaryTree t
(I could have written this in Java, but I can't bear writing subclasses to make up for a lack of sum types. It looks pretty much the same in Rust, plus Boxes. In C++, this would be `class Node<typename T> { std::variant<T, std::pair<Node<T>* , Node<T>* >> data; }`... or something like that.)
When I put a bunch of these nodes together, I get a binary tree. But maybe I want to store these nodes in a database, and I need to store them as rows in a table, where a node's children are given by foreign keys into the same table. I could write a whole new type for this...
data RowNode t where
InternalRow :: (Integer, Integer) -> RowNode t
LeafRow :: t -> RowNode t
... but now I've duplicated the structure just to change the references. What if we take the child references as another type parameter?
data NodeF t child where
Internal :: (child, child) -> NodeF t child
Leaf :: t -> NodeF t child
type RowNode t = NodeF t Integer
data BinaryTree t where
MkBinaryTree :: (NodeF t (BinaryTree t)) -> BinaryTree t
Now I get my recursive trees and my flat tables out of the same basic "tile". NodeF is a functor (although I haven't said why, yet), and BinaryTree applies it to itself recursively (using Box as pointer indirection).
The MkBinaryTree constructor isn't doing anything special. It's just wrapping the given node into a new type, like how pointer indirection is necessary in C++ or Rust to avoid infinite-sized types. Similarly, Haskell doesn't allow recursive type aliases.
That's really the key point behind the recursive functors. We're extracting the "child" type out as a type parameter, and plugging it back in recursively to get our unbounded-depth tree back. Everything after this point is about what free monads add to this, which might be less interesting -- but I've already written it now, so whoops...
Why is NodeF a functor? I can write a function that takes a `NodeF t child` and a function from `child` to `stepchild`, swap the children out using the function, and produce a new `NodeF t stepchild`. That's just "fmap" -- and that's why we stuck an `F` on the name "NodeF", as a kind of Hungarian notation.
instance Functor (NodeF t) where
fmap f (Internal x y) = (Internal (f x) (f y)
fmap f (Leaf t) = (Leaf t)
Now, if I pull a random definition of `Free f` from the webs...
-- adapted from https://stackoverflow.com/a/13357359/159876
data Free f a where
Pure :: a -> Free f a
Roll :: (f (Free f a)) -> Free f a
... and inline our `NodeF t` functor into it...
data FreeNodeF t child where
Pure :: child -> Free t child
Roll :: (NodeF t (FreeNodeF t child)) -> FreeNodeF t child
The "Roll" variant is essentially the same as our BinaryTree construction, in that we can construct arbitrary binary trees with concrete values. The "Pure" variant lets us end the tree, but ending in a "child" instead of a leaf value. It'll be helpful to think of this more as a "pause" than as a "stop".
If the `child` is itself a `FreeNodeF t grandchild`, then the whole structure is a `FreeNodeF t (FreeNodeF t grandchild)`, and the canonical thing monads let us do is flatten the nested structure into a `FreeNodeF t grandchild`. In other words, we roll the `Pure` subtree over into the other variant.
The "impure" computation our `FreeNodeF` describes is essentially a branching computation. At each step, we can take a value and replace it with a tree of other values. The next step is performed on each of the values in the new subtree, and so on. At any point, each path can terminate in a final `t` value; but other paths might still keep going.
Essentially, we've structured our computation itself into a binary tree. The upshot is that the free monad lets you structure your computation into any template given by an appropriate functor. The paths through your structure trace the steps over time taken by your program.
"Algebraic effects" seems to just be a fancy/confusing name for resumable exceptions. FWIW, resumable exceptions have been used in the past but it seems they have been seen almost exclusively as a net-negative.
One important difference between algebraic effects and resumable exceptions is that with algebraic effects, the handler gets access to a reified version of the current continuation. The handler can then use that continuation to resume an arbitrary number of times, or not at all. For example, the handler could resume once for each item in a list, essentially using the continuation to map the list to a new list. The handler can even return the continuation to be invoked later, after the handler has finished. This is very unlike the restricted kind of resumable exceptions that you see in hardware.
Algebraic effects are a structured form of delimited continuations, for people who know about that concept. They give you nearly the full power of monads.
I feel like the reification (or in particular the performance characteristics thereof) are more important than the repeatability.
I think delimited continuations are a much more accurate description of what algebraic effects do. There are implementations of delimited continuations in Common Lisp based on the condition system but if you have too many continuations (or if you switch between them too often) you will blow up the stack. Other implementations (of async rather than continuations) use macros to translate source to continuation passing style and then rely on the compiler being clever, which sbcl is ok at.
Note that you can implement resumable exceptions so long as you have a reliable way to do nonlocal transfers of control (eg return-from or go in CL; or some kind of exception which can only be caught by a try that you write and not by someone else’s, though the finally would still run), and some dynamically scoped list of first clsss functions. Implementing delimited continuations is much more involved.
Resumable exceptions are still used in Common Lisp, in the form of the condition system. There have been a few recent posts about it on HN, including one about a new book.
I think the key is that the system lets you specify multiple resumption points, so then the exception handler can make an intelligent decision of how to resume.
So I think this is a good introduction to what algebraic effects are but I think it also misses the mark a bit.
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:
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.
Yeah I agree that morally the interface is applicative or arrow like but I didn’t think that provided an ergonomic programming interface which is most of the point of hooks (and the pain of arrows). Binds couldn’t really changed the shape of their output because of the second type parameter I described. Also, I don’t see a particular reason to disallow the default value of one state depending on the value of something else. You can’t really stop them from depending on eg function arguments in a nice way.
If you haven't seen the Bonsai OCaml library, they provide an arrow based interface for making web apps. I find it very straight forward to use and everything is based around composing functions. A "component" is:
type ('a, 'b) t = 'a Value.t -> 'b Computation.t
and then there's various ways to hook them up and depend on each other.
Gives a bit of an overview. There are some syntax extensions to make working with it a bit easier. But once grokked it seems to fit nicely, and feels like more of a solid foundation than React (imo).
Hooks, and algebraic effects more generally, made a lot more sense to me when I realized they’re really syntactic sugar for a pretty common and useful pattern.
Imagine, for a moment, that every React functional component was passed context as an argument. It’s a black box that you can only interact with through a few public methods - ctx.useState, etc.
For consistency, since we are likely to wrap these in higher order utilities, let’s make them functions not methods: useState(ctx, ...)
Now, it would be annoying to pass ctx around to every prop. So since JS is single-threaded, you could validly use the hack: let’s just store the current value of ctx before calling our component code, then it can be implicit at the call site.
And you might implement this as a decorator (@ annotation) that wraps your component, which you can do in Python. But that’s redundant too since the presence of hooks implies the need for it. So magically imply that wrapper if it’s needed.
That’s it. That’s hooks. It’s the same pattern as “accept a bound function/callback as an argument” except React does the binding for you and places it in the argument list for you. All you need to do is use it.
I have to admit I kind of was confused on how this ties to React when the examples aren’t even in JavaScript. If it’s a useful concept for React but the examples can’t even be clearly expressed in JavaScript the whole point of the article is lost on me.
I would find it much simpler if the computational model of a DOM update is spelled out explicitly.
Because, essentially, we are trying to update the DOM from a single thread - buried in callbacks and need some nifty libraries around that.
Re-frame's domino model is super easy to understand. There's no need to think about 'algebraic effects' or anything like that. Your event handlers returns effects which is data. That's it.
The world moves forward via
Event dispatch
Event handling
Effect handling
Query
View
DOM
That's about enough complexity to deal with manipulation of a tree structure and getting some callbacks.
At least - given I've followed basically all JS frameworks-du-jour since jQuery- this is the simplest model that allows for complex UIs I encountered.
It certainly is opinionated and not completely pure - by design - but oh so sweet to work with.
> There's no need to think about 'algebraic effects' or anything like that.
Re-frame itself would disagree with you -- although that may be because it does its job well enough that you've not needed to worry about algebraic effects.
> Which brings us to the most important point: re-frame is impressively buzzword compliant. It has reactivity, unidirectional data flow, pristinely pure functions, interceptors, coeffects, conveyor belts, algebraic effects, statechart-friendliness and claims an immaculate hammock conception.
Also, this page of documentation calls out effects and coeffects.
> instead of maintaining a running process that holds a reference to a different process, we can simply stop the current process altogether until our effects are finished.
Ooof. That does not seem like an efficient replacement for promises. Blocking the world while you wait for i/o just seems like a recipe for poor performance. Or did I misunderstand this?
Hooks are not functional. The most elementary rule of functional programming is no state and no side effects. If you break these rules and justify your rule-breaking by pointing to esoteric concepts then you're violating the spirit of functional programming.
IO isn’t (typically) functional, and neither is the IO monad. But like reference types in Clojure, it’s how you express “the world is stateful, my program usually is not, but for this exceptional case my program interfaces with the world.” I don’t think React hooks are a good way to signal that, but they’re one of the ways people who prefer FP in real world use do it. I think Redux type solutions are a better signal in some cases, and the rare stateful class component has is better in others. I hope hooks don’t take over the React ecosystem, but if they do I’ll be looking for other solutions similar to earlier React with regular functions and stateful escape hatches.
IO isn't functional and that's the reason FP languages such as Haskell have a system for dealing with functions that have side-effects, that neatly separates the part of the program that is pure and the part of the program that is impure (and does all the dirty work like updating the screen). Impure parts of the program is fenced off from the pure parts. Pure functions cannot call impure functions.
The important part here is keeping portions of your code that has side-effects separate — and minimizing the amount of such code. If code that has side-effects is spread all over your program then you are not really using functional style.
> IO isn’t (typically) functional, and neither is the IO monad
"(pure) functional" is an attribute of expressions, not of concepts. Hence saying "the IO monad is not functional" does not make much sense.
a function that returns an `IO` type, which when executed performs a side-effect, is indeed pure functional though. Hence, react hooks are not pure functional, by the common definition of functional programming.
I don’t think I’ve ever seen anybody claim that hooks are ‘functional’
The fact that each time you call useState in a render method you get back a different value should be a clue.
You have to have a fairly sophisticated/twisted mental model of a react component’s function signature as effectively including each of the hooks it requires to be able to mentally model hooks as pure functions.
Hooks are a clever imperative hack that works only if you stay in the strict subset of JavaScript behavior that react expects, but which lets you get the benefits of some algebraic-effect-like contextual handling that would be easily expressed in a functional syntax, but they’re not functional.
Mind you, an OO person might equally say they are an imperative hack to get you the benefits of dependency injection that would be more easily expressed in an OO syntax, but they’re not OO.
What hooks are is more ‘imperative reactive’, I would suggest.
> I don’t think I’ve ever seen anybody claim that hooks are ‘functional’
That's not quite true. The linked story tries to explain Hooks as algebraic effects. Before Hooks, functional components were explained as "UI is a function of state". After Hooks, they are continuing on FP ideas as the justification or explanation of their design choices: https://overreacted.io/algebraic-effects-for-the-rest-of-us/
I'm not sure what the problem is with using functional ideas as a justification? It feels like you're complaining about a sort of cultural appropriation.
Functional ideas are sometimes good, and they should be borrowed and used in nonfunctional contexts. Like continuation-passing-style got lifted and used to inspire promises and async/await. That doesn't mean languages with async/await are functional, or that programming with async/await is taught as functional, it's just the pedigree of the idea.
React hooks lift some ideas from algebraic effects, and embed them in a decidedly nonfunctional, VERY non-pure-functional context. But the pedigree of the idea is still... algebraic effects.
I’ve personally never thought I was doing functional programming with React. I think of it more like borrowing from the functional programming paradigm to gain better control over state and corresponding UI behaviours and appearances, at least where it’s useful to do so, and otherwise it’s not very functional at all. It feels like a very borrowed term. There is heaps of non functional programming involved in React code bases.
> borrowing from the functional programming paradigm to gain better control over state and corresponding UI behaviors
That's an exercise in futility. If you are rendering read-only screens than you can use pure functions to render the screen. But if you need interactivity then attempting to add interactivity while using pure functions is counter-productive.
I am asserting it is counterproductive. In other words, the code will be convoluted and hard to maintain, which is the opposite of the goal of functional programming.
Pure functional languages such as Haskell have some facilities to do things that are not 100% pure, such as I/O. Similarly, OCaml has effects, but functional programmers minimize their use of such impure facilities.
Effects in OCaml are impure: "However, the effect system also allows for tracking side-effects more generally. It distinguishes impure functions, which perform side-effects, from pure functions, which do not." See: https://www.janestreet.com/tech-talks/effective-programming/
In Haskell impure functions are fenced off from pure functions. So for example, pure functions are not allowed to call impure ones.
If nearly all of the functions in your app are impure (and use React hooks) then you are definitely not doing functional programming.
I didn't think that OCaml had a way of distinguishing functions with side effects... the link you sent seems to describe a prospective extension/system, not a built-in of the language.
> In Haskell impure functions are fenced off from pure functions
That's a great way of explaining it. But at the same time how can we say that Haskell is a "pure functional language" when Haskell programs consist of both pure and impure functions?
Yes they are separated which in other words means you do have both of them. Is that really "pure" then? Or is that more like 'as pure as possible"?
According to this definition a pure programming language could not have any side effect (Not very useful). Maybe as pure as possible is the only interpretation that makes sense, then.
> Functional programming is sometimes treated as synonymous with purely functional programming, a subset of functional programming which treats all functions as deterministic mathematical functions, or pure functions. When a pure function is called with some given arguments, it will always return the same result, and cannot be affected by any mutable state or other side effects.
It is a closely associated idea, but it is not necessarily "core" to what FP is.
Why is it important to not mislead a whole generation of JavaScript programmers into believing they are doing functional programming when they're really not?
I'm not sure that they do. Just because Hooks was inspired by functional concepts like algebraic effects doesn't mean the core React team is suggesting that writing React code is functional programming.
With that said, my point is that it's more important what you can _do_ with a tool, rather than the purity of the tool with respect to an abstract concept. Or, in this case, with with respect to your interpretation of the abstract concept.
I don't think anyone is suggesting with a straight face that React code is purely functional -- this is JavaScript, after all! However, Hooks are a valuable feature, and if they were inspired by algebraic effects, then so be it.
A lot of React developers believe Hooks are functional and that they are doing functional programming by using Hooks. Whether the core React team is responsible for this perception or not I am not sure, but misleading developers about what functional programming is, is not cool.
Hooks are one of the strangest features I've seen in a language yet. I doubt they will be a lasting paradigm outside of the React ecosystem.
I like JSX a lot. The idea of reusable stateless components is totally on the right track.
I've found React to be very hard to debug. I get stack traces that don't seem to start from any code I've written.
I now somehow suspect that the next wave in the JS ecosystem will be doing something like microservices on the frontend - message passing between isolated webworkers? I ponder this because React has not, for me, achieved any level of enforcing a sane way of deciding which objects in memory belong to a given component. Something feels really wrong with the the React model of programming.
Writing a project in React seems doubly frustrating - there's the challenge of a new stack and new programming model, but I don't seem to be getting anything for the trade: the code isn't easily debuggable, doesn't have a great upgrade and maintainability story, the app still has race conditions, memory leaks, browser incompatibilities, tightly coupled components. The problem seems to be that React only gently encourages, but does not enforce, any good practices.