Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

SOLID in React: the good, the bad, and the awesome

Sign upSign InSign upSign InIgor SnitkinFollowITNEXT--ListenShareLet’s talk about SOLID principles from the perspective of the React application. If you’re not sure what SOLID principles are, you might want to read the following first:medium.commedium.comI’m very well aware that there are plenty of articles on this exact topic already, and I know that because I’ve read most of them. And have to say I do disagree with the way the information is provided in those articles (otherwise I obviously wouldn’t have written this). The problem I see is that you’re only told how great and awesome SOLID principles are, without mentioning potential downsides or considerations; are they even applicable in React application? And that’s exactly what we’re going to discuss just now, so don’t stop reading!The very first Principle is generally the easiest one to understand: your component should have a single responsibility, or in other words, there must be only one reason for this component to exist.However, if you switch your brain from the consumption mode to thinking, you might immediately notice one problem: given that any non-leaf component in your application is the root of a subtree, there is an almost certain chance that some of your components will have more than one responsibility — the component has five children and controls the state of three of them. I bet your root App component violates the principle big time!So what do we do? How do we navigate and apply the Single Responsibility Principle? If you look closely you are likely to find two groups of components: ones that are easy to design with the Single Responsibility Principle in mind; and others, some kind of orchestrators that have a lot of responsibility (or control). You can call them smart/dumb components, or container/presentational, I don’t really care as long as you acknowledge this separation.I will call them managers and workers, and I claim that by introducing a simple set of rules you can make all the components of your application respect the Single Responsibility Principle! Let’s define them:Let’s think about those two rules for a second. What does “Managers should never do Workers’ jobs” even mean? It means that Managers should never have their own JSX content, whatever they return should only consist of Workers and/or other Managers, or to put it super simply, they should not have their own HTML elements. As a result, they also won’t have CSS, as there will be nothing to style. You can safely say that Managers represent the (java)script part of your application — they carry all the business logic and control most user interactions.What about Workers who “should not be aware of business logic”? Well, that would be the opposite! They will carry all the content and styling (or HTML and CSS part of your application), with little to no JavaScript — only required for their own isolated existence.I have to note that there is a pretty big caveat when it comes to styling (way too big to include it here), and depending on when you’re reading this, there might or might not be another article from me titled “How styling destroys your React app” (or something like that).Also note, that the fact that Workers cannot delegate jobs to Managers doesn’t mean Managers cannot be children of Workers in your component tree, and we’ll talk about it in the last chapter 😉This principle has an easy-sounding definition: your component should be open for extensions and closed for modifications, and yet it might not be as easy to understand the difference between an extension and modification. From the point of view of the component itself, any extension is a modification. Therefore we can deduct that this principle relies purely on the point of view of the external consumer of your component:“I don’t care what you do to your component as long as it doesn’t require me to change the way I use it!”This might sound familiar! That’s because this principle is the backbone of Backward compatibility, and it’s literally everywhere. Web APIs and design trends will always evolve and you should always expect a need for the evolution of your components, and this evolution should always* respect existing consumers. Let’s see the Open/Closed Principle in practice with a simple example:You were tasked with creating a Button component that will, if provided, display an icon, and so you did it:Sometime later, the product designer tells you: “Dude, we need a button with the icon on the right side!”, what are your actions? Try to pause and think about what would you do.There is more than one way to achieve this new behavior, for example by introducing a new prop iconPlacement with the default value of left/start, which will control the icon’s position. As long as consumers are happy I wouldn’t argue against this, but personally, I would actually introduce two new props and deprecate one:I hope this clears a little bit on what is the Open/Closed Principle and how to implement it in your React application. Note, that this principle is not a silver bullet (HINT: nothing is) and extensions inevitably lead to BIG(ger) files and at some point become counterproductive. Sometimes you might want to go the route of extending the components and creating their subtypes instead:Can you spot a potential problem in this component? Don’t worry if not — we will talk about it in the next chapter.The Liskov Substitution Principle is based on covariance: you should be able to substitute any supertype with its subtype. For those who didn’t take computer science classes in school, the naming might sound a bit weird. If I extend a type A creating new type B, it is counterintuitive to call type B a subtype of A and A a supertype of B, but that’s what it is — remember, trees in computer science grow down too.So what mistake did I make in the IconButton component above? IconButton extends theButton component, in other words, is a subcomponent of theButton. Now, I have two questions for you, the first one you might have already guessed, and the second is a tricky one:The answer to the first question is NO! By omitting the icon property of the Button component I effectively violated the Liskov Substitution Principle. My IconButton component is rather an “orphan child”, and not a classic subtype of the Button. Both types are independent and non-substitutable or, using fancy technical jargon, are invariants of each other.The answer to the second question is also NO! Because it was never my intention to create a subtype in the first place, or in other words, the IconButton was never meant to substitute the Button.This is a great example of Composition vs. Inheritance in programming: the Liskov Substitution Principle is based on Inheritance and React is on Composition (we’ll see it later). The fact that React is based on composition doesn’t mean, however, that there is no place for inheritance. In fact, if you have a look at the Button component above, I intentionally made it a subtype of HTMLButtonElement by enabling two crucial functionalities:As Button component respects the Liskov Substitution Principle**, I can just go and safely substitute all occurrences of button HTML element with it in my application, and that was exactly the plan.So why not making IconButton respect the Liskov Substitution Principle, you might ask. It is certainly possible, but very soon your code will turn into a hot mess with unnecessarily convoluted components and a whole list of things that can introduce a lot of drama in your life.If you’re someone in the early stages of developing React apps, I would just advise you to ignore the principle. If you feel comfortable with React, you should always start with the question: Is this component meant to substitute the base component or element it extends? If yes, make sure you respect the Liskov Substitution Principle, by properly forwarding props, children, attributes, and ref. If not, however — don’t do it!If I ever need to choose one of the SOLID principles you can blindly follow, no questions asked, that surely would be the Interface Segregation Principle: a component should not depend on properties it doesn’t use.At first, this principle might seem strangely simple: why would anyone introduce props the component doesn’t use? If we use proper linter configuration we won’t be even able to do that on purpose, right? What’s the catch?The catch is the recursive nature of this principle, consider the following component:At first, it seems like there are no unused props here, we do use user and that’s the only prop we have. However, recursively, the User type drags a bunch of unused properties, like email and id, to name a few, into our component — something that this component absolutely doesn’t need and that’s where the Interface Segregation Principle is violated. It is closely related to our discussion about Worker components above, that they shouldn’t be aware of business logic. In fact, one can argue that they should not be aware of anything external at all (not always possible).Let’s fix it!Let’s analyze what has changed. The most important thing that we did was remove the import on line 3 in the first example. By doing this we effectively decoupled UserAvatar from the User interface, which in turn greatly enhanced the reusability of our component. In fact, we even removed any mention of “User” from the naming — now our pets can have avatars too! 😻And now, for our final chapter, please welcome the one and only, the Godfather of composition in React, the principle of all principles — the Dependency Inversion Principle! The official definition is somewhat hard to understand, especially at first sight; it’s also the only SOLID principle that has a two-part definition:A. High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).B. Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.If it sounds heavy, that’s because it is. The Dependency Inversion Principle, as you might have noted by my glorious introduction, easily deserves an article of its own. And when it comes to React, I have good news and bad news for you:GOOD: You are using the Dependency Inversion Principle by default without even noticing it!BAD: You are using the Dependency Inversion Principle by default without even noticing it!So, let’s unpack it and start noticing this by far the most powerful SOLID principle that is the foundation of all things React. Let’s start with parsing the name: what is the dependency? It is an external code (module, package, function, hook, component, etc.) that I use in my component. There are exactly two ways this dependency can appear “on my territory”:Import is a direct dependency, which I have little to no control over. If that dependency changes its behavior I’m screwed.Injection is an inverse dependency: even though I depend on what’s passed to me, I control what can be passed with an abstraction — the props interface.Note, how closely intertwined the Dependency Inversion Principle is with the Single Responsibility Principle and the Interface Segregation Principle. Remember, in our badUserAvatar example above we mentioned that the component shouldn’t be aware of business logic (be a Worker); also that we shouldn’t pass properties/information component doesn’t need; the Dependency Inversion Principle adds the third reason why that component is bad — we’re creating a direct dependency, or in other words, our abstraction (AvatarProps) depends on details (User).The second and most important thing you have to note is that the Dependency Inversion doesn’t stop just on passing functions in our components. Literally, every example out there about this principle in React talks exclusively about event handlers, not noticing the most important thing the Dependency Inversion Principle enables. Let’s rewrite our Button component again and see if you can notice it:So, onClick is the obvious one: it cannot be baked in, otherwise it will violate the Single Responsibility Principle, and it cannot be directly imported as it will violate the Dependency Inversion Principle. Do you see any other examples of dependency injection here? What about children? Are they passed as a prop? Check! Do I control their type? Check! And here is the culmination of this chapter:Whenever you talk about the Composition Pattern in React you talk about the Dependency Inversion Principle. They are the same!This is exactly what makes it possible for Workers from the first chapter to be higher in the component tree compared to Managers — their dependency is inversed and they effectively become containers*** without being aware of what exactly they contain, and hence not violating the Single Responsibility Principle.Now, let’s get back to my promise from Chapter L to show you that everything in React is a composition. The way we pass children in React is nothing else than syntactic sugar. Look closely at these components and the way they are used:Components A and B are identical, with the only difference: I’m able to pass data to the component in a more natural (from the JSX’s point of view) way by using reserved children prop.Now, let’s talk about type discrimination. The type ReactNode | undefined is not a law per se, it’s just the most generic type React is able to work with without crashing. But we’re free to govern the type, remember? So, let’s narrow our types and rewrite the sections above:As you can see, the composition in React isn't reserved just for children prop, you can compose your application with any prop name; it isn’t reserved just for ReactNode type, you can compose your application with any prop type. Everything in React is a composition.Purely from React’s point of view, the Dependency Inversion Principle is nothing else than lifting responsibility up to the parent. This makes total sense considering the Manager/Worker pattern we discussed in the first chapter. But, just like with the Single Responsibility Principle, if you switch your brain from consumption to thinking one last time, you will notice quite a big problem again. You cannot just lift the responsibility up indefinitely. In other words, your application will need at least one Manager component; and if it’s only one, just like in real life, you can imagine the level and the amount of responsibility this poor guy will have. This will absolutely result in a disaster from the readability and reasoning point of view.Despite all the incredible advantages and the stability the Dependency Inversion Principle gives you by decoupling your components, the composition has to stop at certain levels, and Manager components seem like a great place to do that. It is generally a good idea, however, to follow the Dependency Inversion Principle as much as possible, or in other words to keep the number of Manager components in your application as low as possible, and only break the functionality when Managers become overloaded. For example, if you start a new Next.js project, you naturally will have two managers per route, layout.tsx and page.tsx. Later, if your page has a lot of features, you can lighten up page.tsx by introducing a manager component for each feature and so on.OK, and that’s a wrap! Luckily for me, there are no more letters in SOLID and I can finally stop this long and painful process of writing this article. My overall conclusion is that despite its roots in Object-Oriented Programming, SOLID principles are very well applicable in React app development, so there is definitely value in knowing them, particularly, in recognizing that a lot of code that we write by default without thinking too much is actually based on these fundamentals.I really hope it will clear things up a little bit for you, and if it will, I would appreciate it if you share this with the world somehow.----ITNEXTWeb Engineer @SpotifyIgor Snitkin--1Jacob FerusinITNEXT--8Juntao QiuinITNEXT--14Martin HeinzinITNEXT--7Kedar PetheinStackademic--Zhimin ZhaninJavaScript in Plain English--3Mahesh SainiinInterviewNoodle--37Coding BeautyinCoding Beauty--28SamsonkinAWS Tip--7Marcos Pereira Júnior--10HelpStatusAboutCareersBlogPrivacyTermsText to speechTeams



This post first appeared on VedVyas Articles, please read the originial post: here

Share the post

SOLID in React: the good, the bad, and the awesome

×

Subscribe to Vedvyas Articles

Get updates delivered right to your inbox!

Thank you for your subscription

×