Is my desire for Dependency Injection (DI) a code smell?

Allow me to wax philosophical for a moment (as I try to build-up a better mental model for Hotwire).

I come from the Angular world (since AngularJS 1.0.8). That’s a decade of “Services” that can all be injected (Inversion of Control, IoC) into other services, controllers, and directives. As such, the idea of encapsulating complexity behind a service is my default thought.

When I started to dig into Hotwire Stimulus, one of my first questions was therefore: How do I apply dependency injection (DI) with Stimulus controllers if the constructor is not something I have control over?

The more I dig into Hotwire, however, and the more I think about the “Hotwire way”, I wonder if — in a Stimulus context — the desire to have IoC is a bit of a code smell?

Here’s my reasoning: in a traditional SPA (Single Page Application), much of the logic — and therefore, the complexity — lives on the client-side. As such, in order to make the complexity easier to manage, we push it all into services and then provide those services wherever they are needed. In Hotwire, we’re supposed to be taking that complexity and moving it back onto the server. As such, many of the services that I might have had in an Angular app should be replaced with some sort of an HTTP request to a server-rendered template that either replaces the current page in some totality or some targeted Turbo-Frame (or DOM node via Streams).

To make this thought more concrete, one of the first “services” that I wanted to “inject” was an API Client. But, what I now realize is that I probably don’t need an API client, I just need to use Turbo.visit(). Because, after all, I don’t want to parse and translate JSON — I just want to grab some HTML over the wire and jam it into the DOM.

Does that connect at all? I’m mostly just talking out loud here; but, if anyone has any feedback, I’d love to hear it. Thanks!

I won’t touch the concept of smells, I even know people who hate the smell of freshly ground coffee beans, so everything is subjective. :slight_smile:

I really enjoy reading your threads with all the first impressions working in a new and, to you, foreign paradigm. You say “moving the complexity back onto the server”, but to a lot of Hotwire (or Alpine or htmx) users it never left. To me, Stimulus is a better structured vanilla js replacement for the jquery style of interactive sprinkles I used to have. But even though the js-specific classes are now data-controller="xyz", the concepts are similar.

To begin with, I implemented Stimulus without Turbo. But to make the server behave in a truly reactive way without using a lot of fetch requests with Stimulus, Turbo is essential. To be happy with Hotwire, you should keep javascript dom generation and json parsing to a minimum. I have no Stimulus fetch requests at all, and in my current app, I think I use Turbo.visit() in one place - I wanted to trigger a page visit with a checkbox instead of an anchor. And even this is probably a mistake, I could have stayed with regular links. Granted, my site is pretty old school, but all of the more advanced features I have on my roadmap (live updates to pages etc) I can achieve with websockets/turbo streams.

Some Hotwire users use Turbo for everything and basically hate javascript. I use Stimulus quite a lot, but I mostly use it to trigger interaction and behaviour the way I would have in the old days. The concept of setting data actions on the dom node itself felt foreign to me at first (and a bit ugly), but it enables a cleaner style of javascript by more or less taking event listeners out of your code. This is especially useful when you need controllers to talk to each other. I haven’t used the new outlets API for this yet, I trigger custom events from one controller and set a data-action="notification:add@window->notifications#displayNotification" on another. Doing this without Stimulus would have involved tedious addEventListener boilerplate, but apart from that it’s all vanilla js.

This answers only parts of your question.

1 Like

It’s funny, I don’t think it would even have occurred to me that you could use Stimulus without Turbo; but, yeah, they are taking care of two totally different concerns. I do really like how Stimulus controllers are seamlessly connected / disconnected from the DOM - that actually feels very Angular-like; but, the fact that Stimulus will simply react to any DOM changes (such as suddenly setting innerHTML) feels kind of magical.

As far as the complexity still being there, I am sure my perspective has more do with the fact that I’m just learning; and, I haven’t really done anything too complex yet. That said, I’ve been starting to play with making a View be accessible on its own as well as in a fly-out menu and — oh boy — that seems to be a whole other level of complexity with how to say in a Turbo-Frame sometimes, and how to break out of a Turbo-Frame in other cases. I’ve been following this whole thread: Ability to override frame-target from server response #257.

I’ll be honest, as I’ve been learning this stuff, there are moments where I just want to flip my desk over and go back to Angular :rofl: but, then I’ll figure some small detail out and suddenly things don’t seem to crazy.

Outlets are on my list of things to explore next.

Thank you for the conversation!

1 Like

For a long time I only used Stimulus, it wasn’t until later I realised the utility of Turbo. In fact, when I joined this forum it was called discourse.stimulusjs.org (before the Hotwire rebrand). (Never used the precursor Turbolinks.) On its own (without Turbo), Stimulus is still my favourite js framework because it basically just removes some of the boilerplatey syntax of vanilla js, but still lets you use the web platform almost as-is. Every vanilla solution I’ve found when googling can be used in Stimulus with tiny adjustments. It’s closer to webcomponents than to frontend frameworks, really.

In some cases I’ve found Turbo Frames to have diminishing returns and adding needless complexity. I was decoupling my global menu recently, going from a Rails application controller method and a partial called from the layout to a separate controller and an new index view to be used as a turbo-frame source (to be called from the layout). But apart from potentially lazy loading slow menu entries, I found out that it added complexity without really adding anything of value, so I scrapped the whole thing. It really depends on the app, but I’m convinced sometimes the right call is just not decoupling with turbo-frames. Just because I could, doesn’t mean I should. (If it makes sense from a business logic standpoint, on the other hand, it’s almost always a good idea.)

Yeah, I’m finding that Turbo-Frames can add a lot of cognitive load to the application architecture since you have to start thinking about the way in which something will be accessed. And, especially if you progressively enhance the app to make only one of those ways the natural path in the application, it can be hard to remember to go back and (for example) stop including your JavaScript bundle in order to test the frame content when Turbo isn’t available.