Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
UI = f(statesⁿ) (daverupert.com)
168 points by todsacerdoti on Feb 16, 2024 | hide | past | favorite | 101 comments


Somewhat unrelated but, I've been doing a ton of frontend coding past few years (coming from a backend/infra background).

I think browser development is an underrated difficult problem. With a lot of crappy half-assed solutions that don't have solutions at depth that the backend ecosystem has.

Like if you get to the essence of it you're syncing two local event loops -- the JavaScript and browser renderer, omitting the JS async queue and webworkers. Then you have a distributed system on top connecting to your backend. And sometimes you have a distributed system across the same local instance of the app, e.g. multiple tabs of the same app.

All this with neanderthal grade tools. At every layer of this there are half-thought tools with inconsistent data models that try to connect different layers of the problem (component rendering, data fetching, event loops, background jobs, etc) without any great way to do so. Straight glue.

Sure most people call some hook with refresh loop or something and call it day, but the problem still hasn't been solved well for building ultra-rich apps.


One thing I learnt going from frontend development, to distributed system database, development, and then back:

These neanderthal grade tools have the same problem: attempting to abstract away unabstractible problems. The MO of said tools are to limit expressiveness rather than provide context or knowledge.

Take the multi-tab syncing you mentioned. Syncing will always take a loop and a buffer, assuming a channel of communication is not a problem. Those can be in a library, in a browser's built-in API, abstracted away, or you can write it by yourself. But when you don't write it, you don't have control over it (e.g. when to sync, how to interlace the syncing with other operations).

A better toolings work in the level of paradigm. Instead of simply abstracting, bring complexity to clarity and provide the learning ramp.


This is the problem that most JavaScript frontend frameworks are trying to solve. Most of them just connect JS and renderer event loops, but some are approaching data fetching and background jobs as well (like React with the experimental `use` hook, or Svelte Query).

They do however come with significant complexity for new users, and if you write React code without understanding the underlying event loops, you'll write code with weird edge cases (eg. overreliance on useEffect). But I do think that they solve the problem pretty well once you got past that initial burden of learning them.


> they solve the problem pretty well once you got past that initial burden of learning them.

Please do share how the canary `use` hook solves: remote data syncing, proper data sharing across the UI, real-time remote syncing, incremental updates, optimistic updates, realtime collaboration, offline only, etc

These are table stakes for good rich apps now. Now your answer is probably going to be to 'use other tools for this' and the lack of cohesion between those tools is what I'm saying is crappy glue. The atoms aren't good.


I think your question is valid! React does have solutions for most of these, but remember that even in Canary, its job is just to sync state between renderer<->DOM events<->data fetching, not to replace your backend.

- Proper data sharing across the UI: Contexts

- Optimistic updates: `useOptimistic`

- Offline only: Can you clarify? React by default is offline only

- Remote syncing, realtime remote syncing, realtime collaboration: `useSyncExternalStore` with a subscribe function that connects to your backend

- Incremental updates: What do you want to update incrementally? You can render the site component for component as they are loaded with Server Components. Otherwise, I don't think React has anything built-in for that.


That’s… not react’s job?

React is all about efficiently taking state and rendering it.

We use mobx for managing state. We use Hasura for syncing. We use react for display. That works well for us and our requirements.


> React is all about efficiently taking state and rendering it.

You sure about that? https://github.com/facebook/react/blob/main/packages/react/s...


That's just patching fetch for suspense stuff, and suspense is literally about rendering state.

Sure, not a pretty workaround, but nah react isn't trying to be in charge of fetching here.


I’m searching for ReactFetch and not finding anything. My 10 years in React tell me that react isn’t a library focused on data fetching, maybe I’m missing something and I would happily discuss it if you point me towards some more information.


packages/react/src/ReactServer.js imports it.


I don’t know what reactsever is either. Was looking more for some documentation to show what it is and how it’s used.


I've never mentioned React, nor really alluded to it (though it is one piece of the complicated problem).


I didn’t quite understand what you meant by “canary `use` hook solves” but you’ll have to excuse me for assuming you were talking about react.


My mention of the use hook (which was first mentioned in a reply to me) was to point out how silly a response it is to offer a random react hook and as the solution to the complex problem I initially discussed. Do read the thread and my initial comment again.


Hasura + React is the dream. https://github.com/Vetd-Inc/vetd-app


react-query's useQuery hook makes the proper data sharing and optimistic updates a breeze.


react-query (or @tanstack/query as its now called) has a few things which make it a little awkward to use beyond very basic fetches:

1. No concept of normalization. Fetching a list of entity type vs fetching a detail of one of those entities means you end up fetching the same data twice since the cache is only per query, not entity. And good luck trying to mutate its cache yourself to force it to cache by entity.

2. No good way to get the data out of react-query and into, say, my mobx/redux store.

3. Its query cache is global, but the `select` cache is per component/subscriber.


I have experienced the same. I am a backend person with 30 years experience who spent 2 years doing only frontend (mostly react) and the tooling is depressingly bad. What is worse though is that (even in this thread) most (all?) libraries scream ‘not our job’, so now it’s no-one’s job then as we don’t have ‘in between’ or ‘frontend/backend sync’ (I avoid middleware on purpose here as that’s already a thing) devs.

Phoenix and other attempts are the future here imho. We have, for our saas with a few 100k users, rolled out a phoenix like (we use mostly go server side) next to our initial react/next frontend and a) our devs prefer it massively (write mostly only go and not typescript!) b) our users prefer it because it’s just more natural. With ssr you can accomplish the same but it takes more pain because we still have to think about state syncing etc; with liveview likes we are back to cgi on steroids: just handle it on the server and forget the client. You still have to do state but it’s much easier to reason about if it’s in one place.


I've recently been writing a Chrome Extension.

It becomes harder when any one of your distributed event loops can be closed/shut down at any time (e.g: content script, service worker, options page).

Chrome Extensions with the newest "manifest v3" do not have persistent "background pages" any more, but have "service workers" that Chrome will terminate every 30s.


Maybe Phoenix Liveviews?


This is a fundamental problem with the standard approach of shipping serialized state and rendering logic to the browser. It simply doesn't scale, and frankly its an asinine approach.

Rendering should live inside the same system as the state being used. In most cases that state lives on a server somewhere, though there are plenty of cases where (usually small bits of) state live in the client exclusively and are never sent back to the server.

The current approach of leveraging build systems and bundlers to generate RPC calls is only going to make it worse. The goal is to effectively hide obscure the split between backend and frontend, in reality all this will do is lead to a terrible debugging process and countless bugs cause by a misunderstanding of the complex DX required. Worse still, given that any piece of state has to either be the responsibility of the backend or the frontend, we can't blur that line completely and will be stuck papering over it with more and more complex solutions.

HTMX has been getting more attention recently. I have no idea if its really the "right" answer for the next few years of web development, but it seems to strike a chord with devs precisely because it goes the way of leaving rendering of server state where it belongs...on the server.


UI=F((irreducibleComplexity + poorDesignChoices + poorArchitectureChoices + techDebt + bad coding + states)^n). The author mixes web and native UI development here to encompass a partial range of possible UIs, there's obviously a lot more complexity in UI development if we expand that list to include AR/VR, CLIs, voice-based interfaces, etc.

But also most software UIs we are building today are overly complicated. Because devs forget f() is supposed to narrow the complexity space to match the users needs, by making illegal states unrepresentable.


TBH - I find the opposite is true. Far more complaints have been of the form “this isn’t an illegal state, the dev mistakenly thought so because they’re not a domain expert - why won’t they let me do this?”

And some illegal states are useful. Letting a form exist with a field the user can’t fill out, along with disabled logic and a helper message, is often the best way to onboard users to your tool. Lots of proponents of making illegal states unrepresentable take those fields away so they don’t have to muck with validation logic, which takes away your best way to explain how to use your tool to a user.


The lesson here should not be "make illegal states representable", but rather "the state of the form widget in the UI" and "the state of the value the form represents" are conceptually distinct. We should explicitly consider the state of the form, the state of what the form represents/controls and the mapping between them. We should think about these separately and represent them in our code separately.


I think you often have an opportunity to model the UI closely after the internal state, in data entry UIs mostly, and you should take that opportunity. It means less code complexity, and the users will expect the UI to closely match whatever database entity you're storing anyway.


That's not criticism of syncing possible states with expected ones.

e.g. Not having a disabled={condition} on some input field isn't going to change the fact that the client wasn't built to handle that state. Either the client was written to expect it or it was not.

All you're saying is just expect the states that make sense which is (A) a trivial claim and (B) something you have to do even if you don't make unexpected states impossible.


i would argue that any state that is useful should not be illegal, but i agree with you that people have often used that phrase to mean what you said


This is the stance that https://acko.net/blog/i-is-for-intent/ takes--that there needs to be some way of representing a temporarily illegal state.


The number of times I’ve started typing my email address into a form and the form yelling in DANGER ALLCAPS RED: “INVALID”.

Yeah I know “a” isn’t a valid email address, but let me finish typing lol.

This is for everyone. Please, please leave HTML form behavior alone as much as possible. Whatever it is you’re changing, you’re probably wrong.


The right way to handle this is - store both the user input and the validated value (potentially null) and an error state (potentially null.) Then, when the user takes action to submit the form, validate and set the error state if needed. That way you’re not constantly setting the error state on typing, but when some concrete action is taken you can signal what went wrong.

Admittedly, anecdotal, but most of the guys in my career who have been really into UI = f(state) only consider the validated state, and wish the stuff that’s purely user input and never sent to your backend would just go away.

And when you consider that state is both what you want to send to the server, and a bunch of intermediate state that only ever means something to the client, the UI = f(state) idea (while true!) doesn’t seem all that helpful. Technically every app is a function of state, but if your definition of state is that broad what help is the idea?


Yeah it’s more useful to think of unvalidated state and validated state. Use different types if you can.

What I don’t care about is if the state is from a prop, a value inside a DOM object, a global…

If I have to know where the state comes from, the code is smelly


I am growing into the idea that everyone should try and build some games. Just a "simple" card game like solitaire would suffice for a lot of the lessons.

Biggest one being that anything you want to be able to do visually has to be represented somewhere as state. Pretty much period. And this state will be far more complicated than the simplified state diagram that you will come up with if you are only modeling the intended actions of a game.

If you are lucky with extra computing power, you can layer your code so that the core rules of the game are able to be written independently of how you represent the cards in play. But I assert that you will often be surprised that accomplishing this separation is more code than the alternatives.


> Just a "simple" card game like solitaire would suffice for a lot of the lessons.

I've written an actual game, a 2D platformer.

If there's a domain where it's easy to demonstrate that the view is purely a function of the state it's precisely in gaming. In many games you can toggle (in dev or even for some during gameplay) between "the real view" and, say, wireframe view of the game (with lots of debug infos added too for example). Or you can toggle between a 3D view of your game or a top-down 2D one. Or you can toggle between different types of rendering (as in Diablo 2 where both a 2D and fake 3D view where available and the modern Diablo 2 ressurected, where you can you can show... The 25 year old view if you want to, at any time).

Many games are 100% deterministic and the entire game user(s) played can be recreated from only a record of the user(s) inputs (which is why some save files for long games can be tinier than tiny). And it'll replay the same on different screen configurations / different platforms.

The view in a game at time n is literally f(state n).


Certainly! That was indeed my intention here. The big learning I'm intending is just how much state that people take for granted is necessary to track.


I'm regularly wondering why games are not more talked about in software engineering. Even in the early 2000s games were:

    - more performant and responsive than most company applications
    - while displaying complex real time geometry
    - pretty well engineered
    - fun and engaging
    - didn't require reading 500 page manual to get by


respectfully you haven’t written enough games

> real game

turn-based and real-time games have different kinds of complexity, so a platformer won’t expose you to all of it.

turn-based games are roughly going to give you an easier time separating the game code, but the UI code is going to feel like a debilitating slog.

> Many games are 100% deterministic

Until you use floating points, networking, or different platforms.

This is more likely to be true in turn-based games, and super unlikely in anything real-time and multiplayer.

Most games like that periodically sync state instead of relying on applying actions on each client


Are you saying that IEEE 754 floating point (which everybody is using) is not deterministic?


For 32-bit x86 it's certainly not. The C standard permits intermediates to preserve excess precision, and the only way to avoid that is by flushing to memory, which is slow. On Windows you usually get 64-bit intermediates and on other OSes usually 80-bit.

64-bit x86 and modern non-x86 architectures are usually deterministic for primitive operations at least, but libm differences abound.


Simple operations like addition and multiplication are generally bit-for-bit deterministic, even across different architectures. More complex operations like fused multiply-add can have slightly different rounding behavior. But the big issue is with the more complicated operations, like trigonometric functions or non-integer exponentiation. Those often have differing implementations between platforms, and the only way to guarantee determinism is to essentially roll your own sin/cos/tan/sqrt/etc.


You have to be careful with comparisons. In JavaScript, for instance, `(0.1 + 0.2) != (0.3)`.


Well that doesn't make it non-deterministic in behavior.


(a+b)-a doesn’t always equal (a-a)+b

It’s different behavior if the order of operations are non-deterministic.

Plenty of room for such things in games. Most of the time the effects are too small to notice. They can chaos-theory out of control, though.

So real time games nearly universally sync state instead of relying on deterministic synchronization via events/actions.

For a good read, look up lockstep deterministic networking.

There’s way too much nuance here to be so dismissive


Chaotic behaviour is deterministic


Weird how I write complete thoughts and supply references but this site full of tech geniuses mostly produces downvotes and single-sentence drivebys.

Come back when you have things worth contributing.


I’m a theoretical physicist, not a tech genius. Perhaps that’s the mode matching problem here. I don’t trust your 23 hour old account though :)


> I don’t trust your 23 hour old account though :)

This might explain why you aren't a genius. He's not saying anything that hasn't been featured on HN before.

This is a popular article: https://www.gamedeveloper.com/programming/1500-archers-on-a-...

It details how Age of Empires decided to make the game deterministic, how this had significant benefits, and how it's extremely difficult to do:

> At first take it might seem that getting two pieces of identical code to run the same should be fairly easy and straightforward -- not so. The Microsoft product manager, Tim Znamenacek, told Mark early on, "In every project, there is one stubborn bug that goes all the way to the wire -- I think out-of-sync is going to be it." He was right. The difficulty with finding out-of-sync errors is that very subtle differences would multiply over time. A deer slightly out of alignment when the random map was created would forage slightly differently -- and minutes later a villager would path a tiny bit off, or miss with his spear and take home no meat.

Do you think you look good by announcing that, since you don't know the subject you're discussing, you're not going to trust someone who does?


I don't trust new accounts whose instinct is to insult me. I don't need to justify that heuristic. Also I was right, chaotic behaviour doesn't mean non-deterministic, which the quote you posted affirms.

So not only was I insulted, I was insulted even though I was right. Also I have studied chaotic dynamical systems, so I have formal education in this.

The interesting thing about chaotic systems is that they have very divergent futures for very similar inputs, not that they're random.

Also I don't really care about being considered a genius, and I think it's sad that anyone would want to be thought of as one.


Saying stupid things is pretty likely to draw insults. They were obviously warranted here.

> The interesting thing about chaotic systems is that they have very divergent futures for very similar inputs, not that they're random.

If you were paying attention... that's exactly what ape_key was referring to. The problem under discussion is that very similar inputs to the game logic lead to radically different states of the game at a later time. The nondeterminism is what gives rise to the fact that the inputs are "very similar" rather than "identical".


Insults are warranted because I affirmed what he said? What? You’ve been a member of this site for 12 years. Maybe you are a bit too jaded from your interactions here. I have reported your comment


People who say the universe is a simulation never consider the graphical overhead required. It'd be so annoying to make, I don't believe even an alien species would have the patience for it.


This is a common misunderstanding. You don't have to simulate the universe, you only have to simulate the minds that believe they are observing the universe. You are then by necessity merely simulating their perceptual facts rather than all of the physical states that eventually produce a perceptual fact. This is considerably simpler.


AI had the patience, Mr Anderson


It's simple. You just create set of rules that will eventually end up in earth being created. Just requires you to be God


They (we?) could use AI for that, why would you write it by hand?


"Red shift is a feature, not a bug!"


I would love to try something like that, but I have no idea how to model complex game logic like that in data, especially if it's decoupled from the UI state =/

For example, to make even a "simple" game on something like Boardgamearena, you're supposed to create a state machine: https://www.slideshare.net/boardgamearena/bga-studio-focus-o...

I don't even know what that is, and it's a lot more complicated than anything I'd ever seen on the frontend in the HTML/JS world. (To be fair, that's probably because I never had proper CS training. I'm just a self-taught web dev.)

But it seems to me that even simple games require creating really complex engines that aren't really similar to the businesses needs I've coded for in the past...

Not disagreeing with you at all, it all just seems so overwhelming compared to simple stuff like HTML/CSS/JS.


That's sort of the point.

A lot of people got drawn into the industry in its latest boom and found themselves glueing together remote services using an ever-changing collection of heavy frontend frameworks and never experienced actually modeling an application from top to bottom. It doesn't need to be a game, but doing more of the dirty work that "[you] have no idea how to model" andis "more complicated than anything [you]'ve ever seen" can help induct you into a much richer and more cognizant craft practice.


This isn't so much the old argument of (say) Next.js v HTML(x), but of the different degrees of state complexity required in a typical business app vs an actual game (at least to my mind).

To me, it's a lot clearer how to deal with a traditional ecommerce database setup like Northwind Traders (https://en.wikiversity.org/wiki/Database_Examples/Northwind). I've had to do that many times and know how to model such a thing.

I have no idea how to start modeling the state for a game like Wingspan (https://boardgamearena.com/gamepanel?game=wingspan), much less something more complicated, like Magic: The Gathering, where any of several tens of thousands of cards can interact with each other in novel ways, maybe even leading to infinite loops and such. In my mind it's like a giant if-then flowchart with a billion flows. I wish I could see the source code for the rules engine of such a thing, but with a step-by-step tutorial...


The Magic the Gathering Comprehensive Rules are a fun read: https://blogs.magicjudges.org/rules/cr/

It's in English rather than computer code, but it sets out the data model and game loop in a way that should feel pretty familiar to programmers.


I’d be deeply surprised if a webapp requiring Next.js doesn’t have a state machine in any components.

If you’re past the complexity threshold for Next.js, you’ve run into what’s really probably the most fundamental construct in computing.

They’re not that complex. They’re just presented in academic language.

If you can program professionally, you’re very capable of self-studying a bachelor’s degree’s worth of CS.



You absolutely should learn about state machines.

Also event-driven programming. (especially the idea of delaying effects, e.g. so in an RPG someone can perform a Reaction which disables the effects of an Action performed by another character before they’re applied).

Learn about stacks.

When you’re ready to make your brain bleed a little, try designing on paper the data structure to make D&D turns work.

Hints: player initiative is a circular array, reactions are a stack, actions/reactions are events. Actions are events which modify characters, reactions are events which modify other players events.

Ignore abilities which e.g. create new terrain or whatever. Ignore movement, positioning, targetting entirely.

This will be fun!

Also I can recommend the free book Game Programming Patterns. It’s useful for programming concepts even if you don’t make games


I agree: take the time to learn about state machines.

I remember the first time hearing about them and being totally confused by the wikipedia page. I think the first article that made sense to me was this one, because it used a webapp example: https://www.infoq.com/articles/robust-user-interfaces-with-s...

And then developing a few sample applications, especially with Elm or another language with custom types, really completed my understanding. The diagrams and math notation are more intimidating than the actual code.


Nah, diagrammatic reasoning is part of it. Separate yourself from syntax.

Doodling on paper (or just mentally envisioning a data structure) is way more fun than writing actual code.

I certainly would dissuade the OP from trying Elm. Not as part of learning state machines at least. Too many things at once and not necessary.


Thanks for sharing that link! I've tried reading about them in the past and always walked away more confused, but this article seems more readable. I'll give it a thorough read this weekend. Thank you!


You might be very surprised at how achievable it is to build a game, as long as you start small and limit your scope. I’d to steer clear of turn based games if you are starting out, as they generally entail a lot of complicated UI.

Instead I would attempt to do some kind of action game with the 2D canvas context. You might start by just trying to get a dot moving around with the keyboard arrows. Do a little research into a very basic “game loop”, which is basically.

function update(){ //handle the input //do some other stuff }

function draw(){ // render code here }

Once you get a ball moving around, tweak you program to add something else, like giving the ball gravity. Then add something else, like making it bounce off the sides of the canvas. Keep doing this until you have a basic game. You really only need to know a handful of techniques to get something basic working, and from there you’d be surprised at how far you can go. Don’t worry about code quality or trying to do it right, just try to figure out how you can get the game to the next step.


Even better, make a simple networked game. Socket.io makes it quite trivial to send messages. What I found works really well is keeping state as a series of mutations. Then a client emits a mutation and its broadcast out, if the mutation can’t apply, then it gets discarded and the server sends its copy of the state to the client that emitted the bad mutation. Makes it really hard to cheat but only requires sending mutations to the clients.

https://matthewc.dev/projects/vuex-sync-p1/


I'm in the camp of f = ui(state), and the reason for this is the extreme of streaming games where UI = frame buffer. I'm inventing my own framework for radically simplifying traditional Web apps via RxHTML which works great for crud apps. However, games requires more insight into state machines and what-not.

In terms of the logic, I wrote an entire platform to simplify multi-player board games which I'm evolving to tackle various businesses. https://www.adama-platform.com/


Honestly, feel free to do it in HTML/CSS/JS. The goal isn't to go as hard as you possibly can, simply to start realizing how much more "first class" knowledge of items on the screen you probably want.

One thing I will caution is to not try and lean into making DOM layout do what you want. Represent the different elements as divs that are absolutely positioned, and you can go a long way. You'll probably still want a lot of seemingly duplicated data in places that you control directly, but that is part of the point.


I'm far from an HTML expert, but there are a few things that absolutely need to be done The HTML Way™:

scrolling - both in terms of event dispatch, and in terms of sizing (incorrect absolute positioning can break this), possibly focus as well. I don't pretend to understand under what exact circumstances the spacebar works for pagedown, but it's certainly something that fails often on fancy webpages but works intuitively on simple ones.

copy-paste. No, middle click is not just another way of meaning ctrl-v, I want the correct buffer dammit!

rich text editing is poorly supported however you do it, but be sure to have a fallback to a plain text mode that uses the browser controls


I mean... yes, if you are building a webpage, you should consider the capabilities of the web browser.

I'd be surprised if copy/paste is something you want in your game. Scrolling for a game is again something you probably have specific ideas on. You could make it so that a side scroller builds everything so that html scrolling gets what you want. Odds are high that you'd have an easier time taking over a lot of that on your own. (Indeed, this is essentially how popular mapping "tiles" work. You don't have all tiles in the DOM, but play with the ones visible to work as desired.)

And what I mean for absolute positioning is more in line with what I do in https://taeric.github.io/cube-permutations-1.html. And even there, I do not keep some things in state such that I have no control over the actual path a face travels as it moves into its new current spot.


I think `view = fn(state)` is mostly right but how it is implemented is wrong. If we look at react, everything is nice and declarative until you register event handlers, at which point you enter imperative land to hand manipulate state.

I think what is more accurate is: `view = fn(event-stream)`. Events include but not limited to:

- User events

- Prop change events

- Local state change events

- Global state change events

When we switch the paradigm to be about event streams, all of a sudden there is no bifurcation between prop, local state, global state, and user events -- it is all treated the exact same.


The purpose of `view = fn(state)` is to protect you from O(n*m) complexity scaling if you handle each event in isolation.

For any given part of your UI, you'll have n events to handle ("UI started up for the first time", "x changed", "y changed", "checkbox toggled", "undo", "redo", "file loaded"), and m invariants to uphold ("while the checkbox is checked, the user cannot edit y", "while x is less than y, the UI should display a warning", "this label displays the current value of y as a localised string"). If you try to manually maintain each invariant when handling each event, you'll find it works for simple cases, but it falls apart for larger values of n and m.


This sounds like `state = reduce(state, event)` and `view = fn(state)` where the view has UI handlers that dispatch events rather than mutating anything.

Elm is probably the simplest example of this paradigm top-to-bottom, but you can wire up something pretty simple in vanilla React with just useReducer and useContext.


I like to think of it as `view, effects = fn(state, events)`, where effects are any subsequent events or instructions that come out of a state change. I think of it very close to how you're outlining it here, though, and when I'm thinking of effects I'm generally also thinking of it as a set of things that can fit into an event stream like you're outlining.


All that you've done there is allow different event-streams resulting in the same state to be represented in different ways. Which is just bad design.

Technically you could also have the even worse situation of equal event-streams leading to different states, but being represented the same way. If you think that won't happen you don't understand concurrency well enough.

If you want to deal with event streams I'd start from the basic state machine:

    state :: event -> state


The list is missing URL query params, arguably one of the more common yet underlooked places to store global state in web apps.


For help in modeling the states and taming the complexity:

https://stately.ai/docs/xstate


(Creator here) Thank you for the mention!


Admittedly I haven't read this article, but the title reminds me of staltz's excellent conceptualization of interactive interfaces:

The user sees screens and generates events.

The computer sees events and generates screens.

It's a mutually observing cycle (hence the name of his framework, Cycle.js).


This is the drive behind comonadic UI frameworks in functional programming - streams of mutual recursion.


> Loading - A fetch is happening

Loading/Fetching can have multiple states. Some I can think of are:

1. Fetching data when there is no data 2. Fetching data to replace the data, but the data on the screen is not invalid 3. Fetching data to append to the existing data

All these warrant different states imo. I have some screenshots on Loading/Fetching here: https://rahulkrrrishna.xyz/notes/2024/01/01/loading-vs-fetch...

I like what `react-query` does. They toggle `isLoading` to true when the data is invalid/nonexistent. All subsequent fetches toggle a different state called `isFetching`


Whenever someone mentions "UI is a function of state", I think about transitions and animations. Is every frame in a transition also a function of state?

I tried adding a transition to a dialog box in React sometime back. When the user tries to delete some data, a confirmation dialog shows up. It disappears(with CSS transitions) when the user confirms. Deletion happens before transitions are complete and the user sees a different, unexpected state in the dialog for a flicker of time.

This is solved if all the data inside the dialog is actually a deep copy of state.

If this was not done via react, we could've prevented the dialog content from being bound to the state

Should UI really be a function of states?


I finally grokked `view = fn(state)` once I understood that the `state` argument encompasses all possible inputs. Your global store, each component's internal state, the text selection, the mouse cursor position, the user's cookies, the progress of an animation - everything listed in the original blog post is just state, and the `fn()` doesn't necessarily need to care where it comes from.

The reason this is counterintuitive is that UI frameworks insulate you from some types of state. When you write your own `fn()`, it's only receiving part of the state, and it's only defining part of the view. In the browser, animations are a good example of this; other examples include keyboard focus, the size of the browser window, and most of the behaviour of native form controls.


> Is every frame in a transition also a function of state?

The start and end state are functions of state. The animation itself is what the element itself does when it's in the process of changing (like kicking a rock down a hill). The scroll position of some text is like this as well, it's internal state that you typically don't need to reify as application state.

You could make these part of the state where necessary, but it often isn't.


I’d say there are two common patterns for implementing behaviours like transition animations in declarative UIs.

The platform and/or the engine that is doing the rendering of your declarative UI might provide facilities to describe animations declaratively as well. In web development, we have some support for animations in CSS. It’s also fairly common, if you use a component-based rendering library like React or Vue, to see some kind of Transition component, which typically wraps the UI components that should change during the animation and specifies the required transition behaviour via its properties.

More generally, application states can be part of the “state” that your UI derives from. The linked post gives several examples of this, but a very common one in web apps is having a loading flag when content has been requested from some remote source and isn’t yet available. Whatever code you use to fetch that content also sets the loading flag in the meantime. Then the declarative UI code just needs to check the flag and render whatever spinner or other effect it wants to show during that time.

The same idea can be used to implement general transitions/animations. Your state can include something like an “is opening” flag, to indicate that a transition is in progress, and for effects that need to vary over time, also something like a time index or percentage complete. The code responsible for the transition behaviour then sets the flag and starts/updates the timer throughout the transition period. On the rendering side, the declarative UI just needs to check if the flag is set, and if so, render the required effect for the current point in the transition.

The key point in this second pattern is that some part of your code that isn’t the main declarative UI is responsible for defining this behaviour via the application state. Then the UI rendering code just takes that extra information into account like any other part of the state.

I think the problem in your deletion example is that you had competing representations of what was happening in your system. Apparently you had code that was doing the actual deletion and updating that part of the state immediately, but independently you also had CSS doing the animation, so the visible effects in the UI weren’t synchronised. As you said, you can handle that by making sure the old state you need to render the UI until the animation has finished is preserved somewhere and used for rendering during that time. Maybe that means making a temporary copy and using that for rendering until the animation is done, or for a quick animation, maybe a pragmatic choice is to defer triggering the actual deletion behaviour until the end of the animation.


Both patterns are valid, but do they both follow UI as a function of state?

In the approach where we maintain a temporary copy, should we count the copy as part of the state or should we call UI = f(currentState, oldState)?


I’m not sure there is any fundamental difference. IMHO the defining characteristic of this declarative UI style is that the rendered UI is regenerated from the logical state of the system that comes from somewhere else and not stateful itself.


It is getting to the point that I feel we need a rule of the form: your UI is rejected unless you can tell a new user how to succeed with it over the phone.

To my mind this means at least: fewer modalities, no hidden menus (hamburger), and, many other things.


So, I'm in the camp of UI = f(state) because I look at things like stadia/app-streaming as an obviously better way to build software faster. However, there are challenges, so I've got an experiment with a new UI framework called RxHTML that basically treats the browser as a function of a stream JSON document and local view state with a tiny language to manipulate the view state. It works shockingly well, and the goal isn't to be 100% of all use-cases but to make 90% super easy for anyone to do.


It’s just state. A big state, but just state.


Unless I missed it, the author doesn't seem to provide an argument as to why the function is exponential. It just seems to be a rant about how much state there is. Sure, there is a lot of state, but much of it is "gated" and you don't need to program an exponential amount of logic to get it all to work. To me it's more like UI is the art of turning f(states^n) into f(n*states). With clever modularity and some intuition about how users think, it's a feasible optimization.


Shower thought I've had recently: would we not be better off modelling a UI as Actions, rather than states? Each action changes this part of the markup, makes these requests to app state / client DB / remote server.

This also maps better to a user centric conception of the app.


Here's how I model it: the view is a function of state; and actions create a new state, which re-renders the view.

  state -> view -> actions -
                           |
    ^-----------------------
Actions can be from the user via the view, but it can come from elsewhere, for example, websocket events.

I like what another commenter said about a "stream of events", though in my mind an action is more limited than an event. An action has no event handlers, it can only dispatch state changes (or other actions).


If you ascribe to the unidirectional data flow philosophy a la Redux, then actions are just transformations between immutable state representations.


You've just described Redux/Flux


Then my description isn't very good because I'm not imagining redux at all.


I think UI = f(state) can only be 100% true in immediate mode UIs? That you can literally have a pure function that takes state and user input and returns a pixel buffer to put on screen. That seems not possible for retained mode UI, which most UI development are?


You can have an immediate-mode frontend to retained-mode state; see React, Compose, SwiftUI, etc. In this case, the retained state is an implementation detail, but the goal of the framework is to render your UI function deterministically whether it's updating or creating fresh state.


I was hoping he was actually going to formalise the notion


Sounds like Cycle.js




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: