Hacker News new | ask | show | jobs
by edvinbesic 3752 days ago
Can someone explain to me the usefulness of "stateless" components? Doesn't thing just mean that you have moved the state outside of the components themselves, they are not really stateless.

For example, their dropdown menu takes in an open={false}. This just mean that you now have to keep track of if this menu is open or closed outside of the menu component itself. Or, you have to write a wrapper menu component that emits something more useful and maintains that state.

Doesn't some state belong in the component itself? Certainly there are cases where this makes sense, right?

6 comments

The way I like to put it is that stateless components are great because they don't have a mind of their own. Stateless components have one responsibility, and that is to encapsulate all of the UI details of a component, but not the functionality of that component.

The dropdown component is a great example actually. It is a minor convenience for the dropdown component to have it's own "open/closed" state. However, when you need more explicit control over the dropdown in a certain portion of the interface (i.e. this dropdown should open not only when I click on it's trigger element, but also when I enter the konami code on some other portion of the site) then it becomes a less minor inconvenience that the open/closed state is actually not in your control.

In general, I do create stateful wrapper components for things like this. A single stateless component that handles common UI details can be wrapped by multiple stateful components that handle functionality. Here's a simplified dropdown/tooltip example.

HoverCard component: A stateless "floating div" component that can be styled and positioned around a trigger element. Handle things such as listening for events (Esc pressed, click or hover over the trigger, detect clicks outside of the HoverCard) and fitting the HoverCard onto the screen if there is some overflow.

Dropdown component: A stateful wrapper component around the HoverCard component with custom functionality. Manages it's own "open/closed" state which is toggled via the event callbacks (onEsc, onClickTriggerEl, onClickOutsideHoverCard) it supplies to HoverCard.

Tooltip component: Another stateful wrapper component around HoverCard which is very similar to Dropdown but with different HoverCard styling and slightly different "open" state management. For example, it uses the onMouseOverTriggerEl and onMouseLeaveTriggerEl HoverCard callbacks instead of the onClickTriggerEl HoverCard callbacks to toggle its open state.

The cool thing is that they both get all of the benefits of HoverCard.

You generally have three choices when it comes to state. You can either use something like an event bus, raise events for others to change their state, move the state to the top and manage all those changes at once, or have something that manages the state of children.

An example would be if you have accordions setup that causes any open accordions to close when a different one is open. If you go with an event bus, each component would setup listeners for something like 'accordion:opened' set their state to close if the opened accordion wasn't theirs. If you move state to the top you'd set all other accordions to closed based off the event and rerender. The last would be an accordion group component that manages all the state for the children, which ultimately is a more localized version of moving state all the way to the top.

Stateless functions just take input and give output without storing anything. This would be accordions with a group that manages the state of the children or by fully moving it all the way to the top, so external functions handle the state. When you use the event bus approach you localize all state within the component and it is no longer a simple given A, you get B.

All interactive components are going to require some minimal local state in order to maintain their function.

But the amount of that state that the actual app programmer need necessarily know and fiddle with is pretty small, and can easily be abstracted away if the framework you're operating under allows it.

I daily use Reagent, which is a very clever ClojureScript layer built on React, and generally the standard pattern there is to have a single canonical state atom that has the stuff you genuinely care about, and the rest can be left to stateless or abstracted local-state-only components that you need only pass the big atom to, or even just a cursor to the parts of it that matter to that individual component.

We have an entire in-house forms library for this that we use like this every day, and honestly once you get used to it it's fantastic and saves yourself a lot of "state hell" like you get with complicated OOP frameworks.

Reagent is basically the reason I'm still a web developer even though I'd really rather be working in native: because declarative FRP is a hell of a lot more fun to work with.

My (beginner level) understanding is that if you move the state up to a higher level then you can generalise the component.

For example a button's state can be controlled by a higher level container component and that state might include variables such as "buttonTitle" = "Hit delete to continue", and onDelete = deleteCustomerRecord. So the core button code can be used in a variety of contexts.

If I'm wrong maybe someone else will correct me.

I guess I understand that for simple component. The checkbox here is a good example where the value is the state, so there is no use in keeping an extra copy of it. Same would be with a slider component or whatever. But when it comes to more complex components I always feel like encapsulating functionality makes the component more reusable and less boilerplate-y which in turn encourages use. It's sort of like convention over configuration.

But then again, maybe it just hasn't 'clicked' with me yet.

Edit: I should say though that having all state external would help in testing the component since you can now simulate every possible state without going into the internals of the component itself.

I'm not religious about keeping components stateless. I have some components where I just found it was getting too complex to keep bouncing the state up and down the hierarchy so I just merged it all into one big stateful component.

If I was a better programmer I probably would have known how to structure it properly to avoid this but I'm not.

Also recently I have gone to the trouble of learning Redux which effectively provides a mechanism for global state and probably that would remove much of the problem with moving state around. But this is the thing about programming - you build your code doing it one way, and 80% into your project find a better way, which you start using. Hmmmm.... now should I get the damn thing built and have the app use two (or more) ways of getting the same thing done, or go back and make the whole app consistently use the better way, or not use the better way and instead continue to use the old way but keep things consistent?

In some way, yes, the state belongs to the component itself, because in the end the component is an object and objects encapsulate that state as long as they are alive.

The beauty of "stateless" components is that you can see them as functions, and should aim to create them as pure functions[1] where the only thing that can change the output (HTML) of your component/function is its input.

Another way to see it is as syntactic sugar for pure functions where instead of:

MyMenu.render({name: 'Something', open: false});

MyMenu.render({name: 'Something', open: true});

You have

var menu = new MyMenu({name: 'Something', open: false});

menu.render();

// Then change the state:

menu.setOpen(true);

menu.render();

// Or maybe

menu.open(); // which calls setOpen and render

[1] https://en.wikipedia.org/wiki/Pure_function

> Doesn't some state belong in the component itself? Certainly there are cases where this makes sense, right?

It depends how pure you want to be. The app I'm paid to write is in clojurescript and follows the convention of keeping almost everything in the global state. The main things I'm missing is focus and cursor state in text fields. The main benefit to doing this session recording and playback. I capture every point of non-determinism in the app as an event so I get full fidelity playback from recorded event streams.

It's more work than just keeping things locally or using two-way bindings but the code to handle the sort of things you'd keep in internal state is simple so handling the events is just registering the cross-app generic handler for the event. I designed the system for capture and replay but the main benefit turned out to be that when something goes wrong I generally know the source of the problem immediately even if I didn't write the code.

To use your menu open/closed as an example, I click on the preferences menu and it doesn't open. I look at the log and don't see `[:preferences/toggle-dropdown-menu :open]`, the problem is that something's eating the mouse event and I have to walk the component hierarchy. If I do see the event, I dump the app state and look in `:preferences :dropdown-menu` and see that the value is `true` instead of `:open` so my handler is broken. If the value is correct, the problem is either in the component itself (click another dropdown and see if it works) or in the computed value chain I'm using to feed the data into the component. For everything but the mouse event, I know by convention which file and function contains the relevant code and they're almost all 5-10 lines of code. Here's what the handler would look like:

    (rf/handler :preferences/toggle-dropdown-menu
      (mw/path preferences-path)
      (mw/set-value :dropdown-menu #{:open :closed})