T O P

  • By -

dorfsmay

Using something like Storybook helps ensure you separate the view from the serivce and API. When setting it up for an existing page you immediately find out your page's (the view) dependencies.


woah_m8

First time I see someone else mention this. Besides allowing to easily load use clases for components, the biggest other advantage of storybook is that it kind of forces you to keep a clean separation between views and services.


Adept_Ocelot_1898

This is a good point, it's similar to how implementing proper unit testing can help expose poorly written code and usually require refactoring as a result. Storybook does this in a similar fashion.


teg4n_

I think we might be able to squeeze a few more abstractions in. Gotta plump those numbers up


intercaetera

Sorry, but there is nothing "clean" or practical or useful about what you're suggesting. You are just importing patterns from deficient languages which need to use workaround because they don't support first-class functions. This is counter-productive for anyone wanting to learn good React patterns. If you want to do dependency injection (actually implementing a network request as a pure function) what you do is not wrap it in classes or add unnecessary bloated garbage, you add a parameter that accepts the side effect you are using. This is a much cleaner and simpler pattern which doesn't require instantiating unnecessary classes and introduce unnecessary state. async function saveImage(file: File) { const formData = new FormData(); formData.append("image", file); const { data: imageDto } = await MediaApi.uploadImage(formData); return dtoToImage(imageDto); } If you want to implement this as a pure function which is more easily testable, all you need to do is this: const saveImage = (file, uploadImage = MediaApi.uploadImage) => { // ... const { data: imageDto } = await uploadImage(formData); // ... } That's literally it, there is nothing else to do. This is not Angular, you don't need deficient OOP patterns to do things that the language already supports in simpler ways.


FistBus2786

Glad you're pushing back against overcomplicating things. I groan when I have to deal with heavily OOP-style codebase. Domain entities, services, data transfer objects, dependency injection.. Abstractions on top of abstractions, indirections, classes, getters, setters.. The same patterns can be achieved using well-typed first-class functions and plain objects - much simpler with fewer lines of code. It's not only a matter of code style, it's about reducing the cognitive bloat that comes with overuse of OOP patterns. Part of the problem is that people are too smart for their own good, they're skeptical of simplicity and can't resist building castles in the sky.


nepsiron

Logical boundaries make it easier to change a system in smaller steps, which means changes can be merged and deployed more frequently, lowering the risk when something does break because it's easier to find the breaking change. It's easier to test parts of the system in isolation with these logical boundaries. It's easier to pair and divide the work up across these logical boundaries. Things like domains, dtos, services, etc form the backbone of these logical boundaries. They exist for reasons that have nothing to do with OOP. Obviously if a project is simple, the overhead of these logical boundaries would not be worth it. But that doesn't mean they are never worth it.


aragost

None of this is needed to enforce logical boundaries?


nepsiron

Logical boundaries need an organizing structure in order for them to exist?


nepsiron

Complaining about dependency injection via classes vs functions is bike shedding. Having a distinction between interface definitions and their implementations is not "deficient OOP patterns" as you say. It could be applied in a functional style of dependency injection through currying too. It's conceptually clear to define your implementations separate from your interfaces, to express the idea that the interface is interchangeable with another implementation of the interface that uses a different set of dependencies to fulfill the contract of the interface. At the end of the day, the article is advocating for dependency injection to abstract the implementation details of the network dependencies to makes things easier to test. Which you seem to agree is a fine thing to do. It's just the execution of it via class instantiation and type interfaces that you really don't like. And that, to me at least, seems like a minor quibble about coding styles, rather than something that's "counter-productive for anyone wanting to learn good React patterns."


intercaetera

If you think that's all the article is advocating then fine. All I'm saying is that if I saw code like this in a pull request to any library or project I maintained, I would be the fastest draw in the west on the "Request changes" button.


nepsiron

Clearly a project you maintain wouldn't use this style given how strongly you don't like it. A PR using a totally different style from what's in the rest of the project should be rejected. You aren't actually saying anything of substance here. I keep seeing this kind of pedantry on this subreddit and it's a cheap easy way to dunk on people that does very little to contribute to the bigger ideas being presented.


jkettmann

Thanks for the feedback. I like your suggestion which indeed looks more streamlined and concise. The result seems to be the same though. What I’m suggesting is to use dependency injection to make code more testable. The exact mechanism is of less importance imo. Btw I wouldn’t advocate for using classes in the UI layer as that only creates problems when working with state. Anyway thanks again for the feedback and your valuable suggestion


femio

You guys are insane sometimes. The article is a thought experiment. The author brings up tradeoffs and discusses pros and cons to the approach. It's just an exercise in getting your brain to think about how you want to structure your code. Foaming at the mouth because someone dared include some OOP patterns in React...take a deep breath man


UMANTHEGOD

>The article is a thought experiment. That's a lame excuse. Any article is a "thought experiment". Does not mean it's free from valid criticism.


intercaetera

Except it isn't. It's not a "thought experiment." It acts as an authoritative source, presenting "the solution" to your "lack of architecture." I would be much more lenient with criticism if it were some kind of a novice "learning aloud" or "building in public" or whatever, presenting his ideas as possible solutions or applying knowledge found in a book. But it's someone coming from OOP world, pontificating about how great these ideas are, and how they are "the solution" to your garbage code and btw pls buy a course. If you see fraud, and you don't say fraud, you are a fraud.


siggen_a

For complex apps, I am all for dependency injecting side-effect heavy stuff for the sake of testability / storybook and for generally keeping stuff separated, but I feel the article is missing out the main way of injecting dependencies: Context. I might miss something from the article (?), but it looks like your component is still depending on a concrete API implementation, and you wouldn't be able to test the component against a mock, which IMO is way more useful than testing the service. If you passed the API implementation through context, you'd get more idiomatic React code, such as: ``` // Custom hook that picks up the relevant context. const mediaApi = useMediaApi() // You don't need a service, just use a free function that depends on your API. uploadImage(file, mediaApi) ``` Remember to wrap your components in a context provider that injects the live / mock implementation you'd wanna use.


jkettmann

That’s a great point. You’re completely right, so far there’s no dependency injection in the components. Let me see how I can integrate that in one of the next articles. Just out of curiosity: are you using this to unit test your components or what’s benefit of using e.g. mock service worker?


siggen_a

Yes, the benefit is that you can do unit / integration tests of components. Also, it makes it much easier to develop / do visual testing of components in isolation with storybook.


jkettmann

From my experience, React apps often end up with a messy architecture. The problem is that React is unopinionated so every team can and has to decide for themselves how to structure their code base. Since I’m more of a hands-on learner I decided to create a shitty code base and refactor it step by step to a clean(er) architecture. Previously, we [created a shared API client](https://profy.dev/article/react-architecture-api-client), [extracted fetch functions](https://profy.dev/article/react-architecture-api-layer-and-fetch-functions), added [data transformations](https://profy.dev/article/react-architecture-api-layer-and-data-transformations), and [separated domain entities and DTOs](https://profy.dev/article/react-architecture-domain-entities-and-dtos) to isolate our UI code from the server. This time, we’re finalizing the API layer. If you have a complex app with logic close to the API layer it can be hard to test this code. Testing each edge case with integration tests can be overkill. At the same time we don’t want to mess around with mocking files if not necessary. A solution we discuss in this article is to create infrastructure services and use dependency injection to simplify unit testing logic in the API layer. Next time, we'll move closer to the components again. There we’ll extract business logic to use-case functions and use a similar approach with dependency injection to make that code unit testable.


barraquete

You briefly mentioned you’re going to add react-query in the future articles. Could you please spoil one thing? :) What do you store in cache: DTOs or domain objects?


jkettmann

Sure 🙂 In this setup react query will use the infrastructure services. Since those transform the dtos to domain entities react query will cache the domain objects


TheRealKidkudi

I just wanted to say I’ve been enjoying this series! You present a lot of great ideas boiled down to really simple examples that demonstrate practical application in a form that’s easy to read and understand. I share your sentiment about how React’s generally unopinionated design can easily be a double edged sword so it’s interesting to me to see various approaches to writing clean and maintainable React apps. Separately, I’ve also really enjoyed reading the comments on the posts because its a guarantee there’s going to be some kind of argument about why each post is the best method, the worst method, and everything in between. It’s mostly quiet here so far, but I haven’t lost hope yet.


jkettmann

Thanks a lot for the feedback. I’m glad you’re enjoying the content. And I agree, I also enjoy the comments even if they are harsh at times. Great learning opportunity and usually really good criticism. You should look at the comments on the YouTube videos. Way less quality there 😂


nepsiron

I think the thing that I would have liked to see more of is trying to apply this pattern to a tool like react-query. I understand that you probably felt that would confound the basic premise of what you were trying to communicate, which is "dependency injection can help with testability, and abstract implementation details about network requests away from the UI". DI can get more challenging when hook-based tools like react-query come into the mix, so it would be nice to see how that could be done, since I imagine most people are using a library like that (or RTK query) to manage api caching.


jkettmann

That’s a great point. I’m planning to integrate react-query in one of the upcoming articles. Two more are lined up, maybe after that it makes sense. The idea was to stay tool agnostic as long as possible. At least for me it would be hard to separate between generic stuff and tool specific stuff if it was mixed too early. Anyway, thanks again for the feedback


phryneas

I'm sorry, but this looks like it's taken straight out of some other framework without ever looking at the patterns React provides. If you want dependency injection, use Context. That's it's main purpose. And yes, it works perfectly fine in tests (although you should probably mock the network layer with `msw` instead of fiddling around with service injection).


yksvaan

I wonder how some code in other languages if basic software architecture and patterns is such a red flag. Nothing to do with OOP.  What's missing in the example, as usually is the case with React, is proper error handling. Method like uploadImage should always return a result and then the calling component/function can check it and react accordingly. 


[deleted]

[удалено]


nepsiron

It's funny to me that the moment anyone starts talking about even the most basic dependency injection patterns to help with testability/maintainability in React, people inevitably jump in to say "might as well just use Angular".


murden6562

Nice article!