<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Rado's Tips]]></title><description><![CDATA[💡 Practical tips and tricks about software engineering, product development, management, Ruby on Rails, React, GraphQL, JavaScript, and other things I have learned from my 22+ year career. 💻]]></description><link>https://tips.rstankov.com</link><image><url>https://substackcdn.com/image/fetch/$s_!-KFF!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fadef3196-df5a-45fe-8d26-37ee9f3866c7_1500x1500.jpeg</url><title>Rado&apos;s Tips</title><link>https://tips.rstankov.com</link></image><generator>Substack</generator><lastBuildDate>Mon, 20 Apr 2026 00:52:38 GMT</lastBuildDate><atom:link href="https://tips.rstankov.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Radoslav Stankov]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[rstankov@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[rstankov@substack.com]]></itunes:email><itunes:name><![CDATA[Radoslav Stankov]]></itunes:name></itunes:owner><itunes:author><![CDATA[Radoslav Stankov]]></itunes:author><googleplay:owner><![CDATA[rstankov@substack.com]]></googleplay:owner><googleplay:email><![CDATA[rstankov@substack.com]]></googleplay:email><googleplay:author><![CDATA[Radoslav Stankov]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Tips for Junior Developers]]></title><description><![CDATA[Many of the habits and skills you build at this stage in your career will stick, so put extra care into nurturing the right habits. My tips on how to develop soft skills and habits to help to learn and grow as a junior developer.]]></description><link>https://tips.rstankov.com/p/tips-for-junior-developers</link><guid isPermaLink="false">https://tips.rstankov.com/p/tips-for-junior-developers</guid><dc:creator><![CDATA[Radoslav Stankov]]></dc:creator><pubDate>Wed, 12 Jun 2024 07:37:46 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/810b70e7-3da8-4308-a0c3-fd89a7a916ce_1024x1024.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I often talk to junior engineers and find myself giving the same advice. Therefore, I decided to write a post about it.</p><p>There are many online resources about coding or finding a job in the tech sector. This post is not one of those. It is focused on soft skills, which I think a junior developer should have.</p><p>Your biggest goal is to learn and grow. Most habits you pick up in the early days of your career will stick.</p><ul><li><p><a href="https://tips.rstankov.com/i/145463671/be-reliable">&#9989; Be reliable</a></p></li><li><p><a href="https://tips.rstankov.com/i/145463671/be-curious">&#129300; Be curious</a></p></li><li><p><a href="https://tips.rstankov.com/i/145463671/work-on-boring-tasks">&#128164; Work on boring tasks</a></p></li><li><p><a href="https://tips.rstankov.com/i/145463671/take-ownership">&#127942; Take ownership</a></p></li><li><p><a href="https://tips.rstankov.com/i/145463671/handling-no">&#128683; Handling "No"</a></p></li><li><p><a href="https://tips.rstankov.com/i/145463671/read-the-pragmatic-programmer">&#128214; Read &#8220;The Pragmatic Programer&#8221;</a></p></li></ul><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://tips.rstankov.com/subscribe?"><span>Subscribe now</span></a></p><p>Let's get started. &#128073;</p><h2><strong>&#9989; Be reliable</strong></h2><p>The first piece of advice I find highly important is reliability.</p><p><strong>ALWAYS follow up on your task commitments.</strong></p><p>Don't leave <code>TODO</code> comments in your code, fully complete your work. <a href="https://deviq.com/boy-scout-rule/#:~:text=The%20Boy%20Scout%20Rule%20can,cleaner%20than%20they%20found%20it.">Leave your code better than you found it</a>.</p><p>When you find that you can't finish a task on time, don't hide it. As soon as you think you can't deliver, <strong>ask for help</strong>.</p><p>You have to show that you are reliable. It is the most important thing to do.</p><p>Your teammates will quickly learn if they can count on you. The more people count on you, the more critical projects you are given.</p><h2>&#129300; <strong>Be curious</strong></h2><p>Be curious and ask questions. Don't be content with not understanding things. This is a nasty habit.</p><p>Try to review other people's pull requests. If you don't understand something, ask. Often, you aren't the only one who doesn't understand. If you are too shy to ask in public, ask directly via private chat message.</p><p>I love working with juniors who ask questions. They are good at spotting overly clever code tricks and unnecessary complexity.</p><p>One of the best ways to learn is by working with other people. Ask people if they want to <a href="https://en.wikipedia.org/wiki/Pair_programming">pair program</a> with you. I've learned a lot from pairing with other developers.</p><h2><strong>&#128164; Work on boring tasks</strong></h2><p>In your early days, you will work on many "boring" tasks like bug fixes or minor UI changes. You should try to find joy even in boring tasks.</p><p>Fixing small bugs will show you how systems are built and how to avoid writing them in the first place.</p><p>Once, I was tasked with keeping our application free of <a href="https://tips.rstankov.com/p/tips-for-dealing-with-errors">runtime exceptions</a>. This was a very useful experience. It showed me how the system was breaking. I built many internal tools to handle errors and started designing my code to be more robust.</p><p>For small repetitive tasks, think about how to automate them or use them as training for your editor skills. I learned my Vim shortcuts by doing a lot of CSS fixes.</p><h2><strong>&#127942; Take ownership</strong></h2><p>By ownership, I mean taking the initiative and actively participating in scoping and estimation, not just executing your tasks.</p><p>Try to be involved in time estimations -- often, you won't be consulted (this is a mistake on your team's part).</p><p>Don't rush to estimate. Take your time to think about the problem. Consult your team members.</p><p>A common mistake is to provide a time estimation that's as low as possible and then not deliver. Be honest and conservative.</p><p><em>...and no, doing an all-nighter to finish a task on time is not proper time planning.</em></p><p>Estimates are tricky; don't hide when you're behind. Embrace it, raise the issue, and look for help.</p><h2>&#128683; <strong>Handling "No"</strong></h2><p>You will be full of ideas, this is great. However, you will often be told "No".</p><p>"<a href="https://www.intercom.com/blog/product-strategy-means-saying-no/">No</a>" is essential in product development.</p><p>This can be quite painful. Don't get discouraged. Understand the reasoning behind the "<strong>No</strong>".</p><p>Sometimes the idea is just not right or has already been tried. Other times, "No" is <em>"we don't have time right now"</em> because of other priorities.</p><p>Show, don't tell. If you have an idea, implement it and make a demo. The ability to present your ideas well is a valuable skill.</p><p>Programmers tend to disagree a lot in abstract and agree when they see the working code.</p><p>When I tried to convince my team at Product Hunt to switch from a standard RESTful API to GraphQL, I didn't just say "Let's move to GraphQL."</p><p>Instead, I implemented a very complex page with GraphQL in a separate branch. I gave a presentation showing all the problems GraphQL would solve for us and presented a migration plan. I didn't hide any of the risks associated with GraphQL.</p><p>Again, don't get discouraged even if your prototype is rejected. Understand why it was rejected.</p><h2>&#128214; Read &#8220;The Pragmatic Programmer&#8221;</h2><p>I'd encourage you to read is <a href="https://pragprog.com/titles/tpp20/">The Pragmatic Programmer</a>. I think every programmer should read it. I used to gift this book to every new engineering hire at Product Hunt.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YTkA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8629a1d8-306c-46c6-bbfe-5d38200cfef8_500x654.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YTkA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8629a1d8-306c-46c6-bbfe-5d38200cfef8_500x654.jpeg 424w, https://substackcdn.com/image/fetch/$s_!YTkA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8629a1d8-306c-46c6-bbfe-5d38200cfef8_500x654.jpeg 848w, https://substackcdn.com/image/fetch/$s_!YTkA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8629a1d8-306c-46c6-bbfe-5d38200cfef8_500x654.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!YTkA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8629a1d8-306c-46c6-bbfe-5d38200cfef8_500x654.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YTkA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8629a1d8-306c-46c6-bbfe-5d38200cfef8_500x654.jpeg" width="350" height="457.8" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8629a1d8-306c-46c6-bbfe-5d38200cfef8_500x654.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:654,&quot;width&quot;:500,&quot;resizeWidth&quot;:350,&quot;bytes&quot;:70169,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!YTkA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8629a1d8-306c-46c6-bbfe-5d38200cfef8_500x654.jpeg 424w, https://substackcdn.com/image/fetch/$s_!YTkA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8629a1d8-306c-46c6-bbfe-5d38200cfef8_500x654.jpeg 848w, https://substackcdn.com/image/fetch/$s_!YTkA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8629a1d8-306c-46c6-bbfe-5d38200cfef8_500x654.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!YTkA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8629a1d8-306c-46c6-bbfe-5d38200cfef8_500x654.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>Conclusion</strong></h2><p>Many of the habits and skills you build at this stage in your career will stick, so put extra care into nurturing the right habits.</p><p>To wrap it all up, I would like to recommend an excellent book that </p><p>On the technical side, master your editor and the programming language you are working with.</p><p>Last but not least, take care of yourself. Work hard but don't overwork. Get <a href="https://www.goodreads.com/book/show/34466963-why-we-sleep?from_search=true&amp;from_srp=true&amp;qid=1xuxAN1SZI&amp;rank=1">enough sleep every night</a>.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading &#128588; To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>You can ping me on&nbsp;<a href="https://www.threads.net/@rstankov">Threads</a>,&nbsp;<a href="https://www.linkedin.com/in/radoslavstankov/">LinkedIn</a>,&nbsp;<a href="https://mastodon.social/@rstankov">Mastodon</a>,&nbsp;<a href="https://twitter.com/rstankov">Twitter</a>, or just leave a comment below &#128237;</p><h2></h2>]]></content:encoded></item><item><title><![CDATA[Tips for Writing Less JavaScript]]></title><description><![CDATA[Practical tips to optimize your web performance by using HTML and CSS instead of JavaScript for common tasks like state management, responsive design, and UI elements.]]></description><link>https://tips.rstankov.com/p/tips-for-using-less-javascript</link><guid isPermaLink="false">https://tips.rstankov.com/p/tips-for-using-less-javascript</guid><dc:creator><![CDATA[Radoslav Stankov]]></dc:creator><pubDate>Tue, 04 Jun 2024 13:23:16 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/0cf594c9-18e7-4733-bab9-09522ec68fbf_1210x862.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I love JavaScript, but like every technology, it has its uses. Many developers overuse JavaScript and use it in situations where better solutions exist. </p><p>In the era of evergreen browsers, we can use new HTML/CSS relatively early after their release.<br><br>In today&#8217;s post, I will walk through the use case in which you can use HTML/CSS instead of writing JavaScript.</p><ul><li><p><a href="https://tips.rstankov.com/i/145289217/store-state-in-the-url">&#128230; Store state in the URL</a></p></li><li><p><a href="https://tips.rstankov.com/i/145289217/use-css-for-hover-effect">&#128433;&#65039; Use CSS for hover effects</a></p></li><li><p><a href="https://tips.rstankov.com/i/145289217/use-css-for-responsive-ui">&#128241;Use CSS for responsive UI</a></p></li><li><p><a href="https://tips.rstankov.com/i/145289217/use-a-proper-input-type-instead-of-a-custom-input">&#128467;&#65039; Use a proper input type instead of a custom input</a></p></li><li><p><a href="https://tips.rstankov.com/i/145289217/use-details-html-element">&#128317; Use &#8220;details&#8221; HTML element</a></p></li><li><p><a href="https://tips.rstankov.com/i/145289217/use-dialog-html-element">&#128444;&#65039; Use &#8220;dialog&#8221; HTML element</a></p></li></ul><p></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://tips.rstankov.com/subscribe?"><span>Subscribe now</span></a></p><p></p><p>So, let's get started &#128073;</p><h2>&#128230; Store state in the URL</h2><p>This could be its own post; it is a pet peeve of mine. &#128517;</p><p>I have commented on too many pull requests about forms like the following, where the state is stored with the React.useState hook, when the best place for this data is the URL.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!na3B!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a068a3e-da6d-4825-a60b-6b092816194f_1612x140.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!na3B!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a068a3e-da6d-4825-a60b-6b092816194f_1612x140.png 424w, https://substackcdn.com/image/fetch/$s_!na3B!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a068a3e-da6d-4825-a60b-6b092816194f_1612x140.png 848w, https://substackcdn.com/image/fetch/$s_!na3B!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a068a3e-da6d-4825-a60b-6b092816194f_1612x140.png 1272w, https://substackcdn.com/image/fetch/$s_!na3B!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a068a3e-da6d-4825-a60b-6b092816194f_1612x140.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!na3B!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a068a3e-da6d-4825-a60b-6b092816194f_1612x140.png" width="1456" height="126" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0a068a3e-da6d-4825-a60b-6b092816194f_1612x140.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:126,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:29822,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!na3B!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a068a3e-da6d-4825-a60b-6b092816194f_1612x140.png 424w, https://substackcdn.com/image/fetch/$s_!na3B!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a068a3e-da6d-4825-a60b-6b092816194f_1612x140.png 848w, https://substackcdn.com/image/fetch/$s_!na3B!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a068a3e-da6d-4825-a60b-6b092816194f_1612x140.png 1272w, https://substackcdn.com/image/fetch/$s_!na3B!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a068a3e-da6d-4825-a60b-6b092816194f_1612x140.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>This way, you don&#8217;t lose your state when the page refreshes, you can send this URL to someone else, and the back button works.</p><p>Of course, it has its gotchas, like only updating the URL when the user stops typing in the search field, etc.</p><p>Even worse offenders are paginations or navigations, which are stored only locally, not reflecting which page the user is on.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!v33S!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaf33277-a77f-44c6-b29f-1ec184403af6_808x118.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!v33S!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaf33277-a77f-44c6-b29f-1ec184403af6_808x118.png 424w, https://substackcdn.com/image/fetch/$s_!v33S!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaf33277-a77f-44c6-b29f-1ec184403af6_808x118.png 848w, https://substackcdn.com/image/fetch/$s_!v33S!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaf33277-a77f-44c6-b29f-1ec184403af6_808x118.png 1272w, https://substackcdn.com/image/fetch/$s_!v33S!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaf33277-a77f-44c6-b29f-1ec184403af6_808x118.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!v33S!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaf33277-a77f-44c6-b29f-1ec184403af6_808x118.png" width="808" height="118" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/faf33277-a77f-44c6-b29f-1ec184403af6_808x118.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:118,&quot;width&quot;:808,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:14583,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!v33S!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaf33277-a77f-44c6-b29f-1ec184403af6_808x118.png 424w, https://substackcdn.com/image/fetch/$s_!v33S!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaf33277-a77f-44c6-b29f-1ec184403af6_808x118.png 848w, https://substackcdn.com/image/fetch/$s_!v33S!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaf33277-a77f-44c6-b29f-1ec184403af6_808x118.png 1272w, https://substackcdn.com/image/fetch/$s_!v33S!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffaf33277-a77f-44c6-b29f-1ec184403af6_808x118.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h2>&#128433;&#65039;Use CSS for hover effect</h2><p>I have seen too much code like the following:</p><pre><code><code>function MyButton() {
  const [isHovered, setIsHovered] = useState(false);

  const className = isHovered ? 'button button-hover' : 'button';
  const onMouseEnter = () =&gt; setIsHovered(true);
  const onMouseLeave = () =&gt; setIsHovered(false);

  return (
    &lt;button 
      className={className} 
      onMouseEnter={onMouseEnter} 
      onMouseLeave={onMouseLeave}&gt;Button&lt;/button&gt;
  );
}</code></code></pre><p>Simple CSS can replace this:</p><pre><code><code>.button:hover {
   ...code
}</code></code></pre><p>And the button becomes just:</p><pre><code><code>&lt;button class="button" /&gt;</code></code></pre><p>With hover, you can do a lot of cool things. My previous post about <a href="https://tips.rstankov.com/p/tips-for-tailwind-css">Tips for Tailwind CSS</a> showed how to use &#8220;group&#8221; and &#8220;group-hover:&#8221; to style children when their parent is hovered.</p><pre><code><code>&lt;div class="group"&gt;
   &lt;span class="group-hover:bg-cloud"&gt;
     When parent element (group) is hovered, I change background
   &lt;/span&gt;
   &lt;span&gt;
     I don't change color
   &lt;/span&gt;
&lt;/div&gt;</code></code></pre><h2>&#128241;Use CSS for responsive UI</h2><p>CSS <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries">media queries</a> and the upcoming <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries">container queries</a> are the way to design responsive UIs.</p><p>In <a href="https://angrybuilding.com/">Angry Building</a>, we have a lot of tables. One could say most of our UI is tables. &#128584;</p><p>On mobile, those tables become a list of cards. We use a regular HTML <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table">table</a>, which changes its &#8220;display&#8221; style on smaller screens.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!teCl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9712ee61-63c1-4403-8aa6-1c782ede4265_1838x1072.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!teCl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9712ee61-63c1-4403-8aa6-1c782ede4265_1838x1072.png 424w, https://substackcdn.com/image/fetch/$s_!teCl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9712ee61-63c1-4403-8aa6-1c782ede4265_1838x1072.png 848w, https://substackcdn.com/image/fetch/$s_!teCl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9712ee61-63c1-4403-8aa6-1c782ede4265_1838x1072.png 1272w, https://substackcdn.com/image/fetch/$s_!teCl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9712ee61-63c1-4403-8aa6-1c782ede4265_1838x1072.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!teCl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9712ee61-63c1-4403-8aa6-1c782ede4265_1838x1072.png" width="1456" height="849" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9712ee61-63c1-4403-8aa6-1c782ede4265_1838x1072.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:849,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:221938,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!teCl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9712ee61-63c1-4403-8aa6-1c782ede4265_1838x1072.png 424w, https://substackcdn.com/image/fetch/$s_!teCl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9712ee61-63c1-4403-8aa6-1c782ede4265_1838x1072.png 848w, https://substackcdn.com/image/fetch/$s_!teCl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9712ee61-63c1-4403-8aa6-1c782ede4265_1838x1072.png 1272w, https://substackcdn.com/image/fetch/$s_!teCl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9712ee61-63c1-4403-8aa6-1c782ede4265_1838x1072.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The way this works is that on mobile screen sizes:</p><ol><li><p>table elements switch to display block/flex.</p></li><li><p>I use the CSS &#8220;<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/content">content</a>&#8221; attribute and &#8220;<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/attr">attr</a>&#8221; function trick to get cell titles</p></li></ol><p>Here is the simplified CSS for this.</p><pre><code>@media (max-width: 640px) {
  .table-component,
  .table-component thead,
  .table-component tbody,
  .table-component tfoot,
  .table-component tr {
    display: block;
  }

  .table-component tbody td {
    display: flex;
  }

  .table-component tbody td:not(.actions):before {
    content: attr(title);
    flex: 0 1 35%;
    display: block;
    white-space: nowrap;
  }
}</code></pre><h2>&#128467;&#65039; Use a proper input type instead of a custom input</h2><p>The <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">input</a> element has many types available and is very well supported.</p><p>The one I have used constantly recently is the &#8220;date&#8221; type; it has a built-in calendar. &#128197;</p><p>I like using <code>&lt;input type=&#8221;date&#8221; /&gt;</code>, instead of a JS calendar library because:</p><p>1&#65039;&#8419; Accessible<br>2&#65039;&#8419; Mobile friendly<br>3&#65039;&#8419; All batteries included<br>&#127942; No extra code</p><p>It's important to note that the &#8220;date&#8221; input type has limitations.. For instance, the popover calendar can't be styled to match a website and only allows for the selection of a single date.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5I9D!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f247d8-44fb-4f04-8048-4a693253ac1a_992x1354.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5I9D!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f247d8-44fb-4f04-8048-4a693253ac1a_992x1354.png 424w, https://substackcdn.com/image/fetch/$s_!5I9D!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f247d8-44fb-4f04-8048-4a693253ac1a_992x1354.png 848w, https://substackcdn.com/image/fetch/$s_!5I9D!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f247d8-44fb-4f04-8048-4a693253ac1a_992x1354.png 1272w, https://substackcdn.com/image/fetch/$s_!5I9D!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f247d8-44fb-4f04-8048-4a693253ac1a_992x1354.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5I9D!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f247d8-44fb-4f04-8048-4a693253ac1a_992x1354.png" width="390" height="532.3185483870968" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/89f247d8-44fb-4f04-8048-4a693253ac1a_992x1354.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1354,&quot;width&quot;:992,&quot;resizeWidth&quot;:390,&quot;bytes&quot;:340326,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!5I9D!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f247d8-44fb-4f04-8048-4a693253ac1a_992x1354.png 424w, https://substackcdn.com/image/fetch/$s_!5I9D!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f247d8-44fb-4f04-8048-4a693253ac1a_992x1354.png 848w, https://substackcdn.com/image/fetch/$s_!5I9D!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f247d8-44fb-4f04-8048-4a693253ac1a_992x1354.png 1272w, https://substackcdn.com/image/fetch/$s_!5I9D!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f247d8-44fb-4f04-8048-4a693253ac1a_992x1354.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>There are many more useful types, like</p><ul><li><p>range</p></li><li><p>color </p></li><li><p>datetime-local</p></li><li><p>month</p></li><li><p>time</p></li><li><p>week</p></li><li><p>&#8230; <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input">and more</a></p></li></ul><p>I can&#8217;t tell you how much JavaScript code I have added to various projects to handle dates. In one project, there were four different calendar libraries. &#129318;&#8205;&#9794;&#65039;</p><h2>&#128317; Use &#8220;details&#8221; HTML element</h2><p>The <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details">&lt;details&gt; disclosure element</a> has been supported in all <a href="https://caniuse.com/?search=details">major browsers</a> for years.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5EYm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bd9544b-7509-4a47-b77d-a95f69f11326_2446x514.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5EYm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bd9544b-7509-4a47-b77d-a95f69f11326_2446x514.png 424w, https://substackcdn.com/image/fetch/$s_!5EYm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bd9544b-7509-4a47-b77d-a95f69f11326_2446x514.png 848w, https://substackcdn.com/image/fetch/$s_!5EYm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bd9544b-7509-4a47-b77d-a95f69f11326_2446x514.png 1272w, https://substackcdn.com/image/fetch/$s_!5EYm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bd9544b-7509-4a47-b77d-a95f69f11326_2446x514.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5EYm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bd9544b-7509-4a47-b77d-a95f69f11326_2446x514.png" width="1456" height="306" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6bd9544b-7509-4a47-b77d-a95f69f11326_2446x514.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:306,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:213263,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!5EYm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bd9544b-7509-4a47-b77d-a95f69f11326_2446x514.png 424w, https://substackcdn.com/image/fetch/$s_!5EYm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bd9544b-7509-4a47-b77d-a95f69f11326_2446x514.png 848w, https://substackcdn.com/image/fetch/$s_!5EYm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bd9544b-7509-4a47-b77d-a95f69f11326_2446x514.png 1272w, https://substackcdn.com/image/fetch/$s_!5EYm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bd9544b-7509-4a47-b77d-a95f69f11326_2446x514.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>It allows you to have expandable/collapsible content.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xu_I!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31003a69-2583-4c0c-889c-aba48da1d9c8_2878x360.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xu_I!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31003a69-2583-4c0c-889c-aba48da1d9c8_2878x360.png 424w, https://substackcdn.com/image/fetch/$s_!xu_I!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31003a69-2583-4c0c-889c-aba48da1d9c8_2878x360.png 848w, https://substackcdn.com/image/fetch/$s_!xu_I!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31003a69-2583-4c0c-889c-aba48da1d9c8_2878x360.png 1272w, https://substackcdn.com/image/fetch/$s_!xu_I!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31003a69-2583-4c0c-889c-aba48da1d9c8_2878x360.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xu_I!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31003a69-2583-4c0c-889c-aba48da1d9c8_2878x360.png" width="1456" height="182" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/31003a69-2583-4c0c-889c-aba48da1d9c8_2878x360.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:182,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:177855,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xu_I!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31003a69-2583-4c0c-889c-aba48da1d9c8_2878x360.png 424w, https://substackcdn.com/image/fetch/$s_!xu_I!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31003a69-2583-4c0c-889c-aba48da1d9c8_2878x360.png 848w, https://substackcdn.com/image/fetch/$s_!xu_I!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31003a69-2583-4c0c-889c-aba48da1d9c8_2878x360.png 1272w, https://substackcdn.com/image/fetch/$s_!xu_I!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31003a69-2583-4c0c-889c-aba48da1d9c8_2878x360.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>No JavaScript, no CSS&#8212;just HTML:</p><pre><code>&lt;details&gt;
  &lt;summary&gt;System Requirements&lt;/summary&gt;
  &lt;p&gt;
    Requires a computer running an operating system. 
    The computer must have some memory and ideally some 
    kind of long-term storage.
  &lt;/p&gt;
&lt;/details&gt;</code></pre><p>With a bit of CSS, you can create a mobile hamburger menu like the one we have at <a href="https://angrybuilding.com/">Angry Building</a>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!EQv8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35c040ea-1e86-4f1c-ba65-23b4c4779c22_1570x1384.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!EQv8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35c040ea-1e86-4f1c-ba65-23b4c4779c22_1570x1384.png 424w, https://substackcdn.com/image/fetch/$s_!EQv8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35c040ea-1e86-4f1c-ba65-23b4c4779c22_1570x1384.png 848w, https://substackcdn.com/image/fetch/$s_!EQv8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35c040ea-1e86-4f1c-ba65-23b4c4779c22_1570x1384.png 1272w, https://substackcdn.com/image/fetch/$s_!EQv8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35c040ea-1e86-4f1c-ba65-23b4c4779c22_1570x1384.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!EQv8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35c040ea-1e86-4f1c-ba65-23b4c4779c22_1570x1384.png" width="1456" height="1284" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/35c040ea-1e86-4f1c-ba65-23b4c4779c22_1570x1384.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1284,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:340717,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!EQv8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35c040ea-1e86-4f1c-ba65-23b4c4779c22_1570x1384.png 424w, https://substackcdn.com/image/fetch/$s_!EQv8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35c040ea-1e86-4f1c-ba65-23b4c4779c22_1570x1384.png 848w, https://substackcdn.com/image/fetch/$s_!EQv8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35c040ea-1e86-4f1c-ba65-23b4c4779c22_1570x1384.png 1272w, https://substackcdn.com/image/fetch/$s_!EQv8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35c040ea-1e86-4f1c-ba65-23b4c4779c22_1570x1384.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The code for this menu looks like the following:</p><pre><code>HTML

&lt;details&gt;
  &lt;summary&gt;
     &lt;svg class="open-hidden"&gt;...&lt;/svg&gt;
     &lt;svg class="open-visible"&gt;...&lt;/svg&gt;
  &lt;/summary&gt;
  &lt;ul&gt;
    &lt;li&gt;&lt;a&gt;...&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a&gt;...&lt;/a&gt;&lt;/li&gt;
    ...
  &lt;/ul&gt;
&lt;/details&gt;

CSS

details[open] .open-hidden {
  display: none;
}

details:not([open]) .open-visible {
  display: none;
}</code></pre><h3>&#128444;&#65039; Use &#8220;dialog&#8221; HTML element</h3><p>The <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog">&#8220;dialog&#8221; HTML element</a> is well supported. Its goal is to simplify creating modals and popovers.</p><p>It has some rough edges, but I still find it quite useful, reducing the amount of JavaScript I write.<br><br>1&#65039;&#8419; Accessible<br>2&#65039;&#8419; Build-in backdrop<br>3&#65039;&#8419; Auto handling of forms (example: auto-focus)<br>4&#65039;&#8419; Stops scrolling of back page<br>5&#65039;&#8419; Auto handle of ESC<br>6&#65039;&#8419; Style as you wish</p><p>You still need to write code to trigger the modal and close it when the user clicks on the backdrop.</p><h3>Conclusion</h3><blockquote><p>There is no code faster than no code.</p></blockquote><p>I love writing code, but I don&#8217;t like writing &#8220;boring code&#8221; that can be handled by the ecosystem I build on top of.</p><p>Whenever I can, I prefer to use elements provided by the system.</p><p>This is the reason I follow the <a href="https://open-ui.org/">Open UI project</a>. I&#8217;m particularly excited about the <strong><a href="https://open-ui.org/components/selectlist/">Stylable Select Element</a>. &#129401;</strong></p><p><strong>What tips and tricks do you have for writing less JavaScript?</strong> &#129300;</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts. &#128588;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>You can ping me on&nbsp;<a href="https://www.threads.net/@rstankov">Threads</a>,&nbsp;<a href="https://www.linkedin.com/in/radoslavstankov/">LinkedIn</a>,&nbsp;<a href="https://mastodon.social/@rstankov">Mastodon</a>,&nbsp;<a href="https://twitter.com/rstankov">Twitter</a>, or just leave a comment below &#128237;</p>]]></content:encoded></item><item><title><![CDATA[Tips for Tailwind CSS]]></title><description><![CDATA[Discover essential tips for using Tailwind CSS effectively, including ESLint integration, how to use it with React and ViewComponent, and when to write custom CSS.]]></description><link>https://tips.rstankov.com/p/tips-for-tailwind-css</link><guid isPermaLink="false">https://tips.rstankov.com/p/tips-for-tailwind-css</guid><dc:creator><![CDATA[Radoslav Stankov]]></dc:creator><pubDate>Thu, 30 May 2024 12:09:22 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d42adaf9-dba1-415c-9153-1eacab507e01_1024x1024.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong><a href="https://tailwindcss.com/">Tailwind CSS</a></strong> is a utility-first CSS framework for rapidly building custom user interfaces. Instead of predefined components, it offers low-level utility classes that can be combined to create unique designs directly in your markup.</p><p>I was very skeptical of Tailwind initially. However, when starting <a href="https://angrybuilding.com/">Angry Building</a>, I wanted to write as little CSS and JavaScript as possible, so I decided to try Tailwind.</p><p>Over time, I changed my opinion of it. It fits quite nicely with my component-driven approach to UI.</p><p>Today, I&#8217;m going to cover tips about Tailwind CSS:</p><ul><li><p><a href="https://tips.rstankov.com/i/145121634/use-eslint-plugin">&#129529; Use ESLint</a></p></li><li><p><a href="https://tips.rstankov.com/i/145121634/search-documentation-with-alfred-dash">&#128270; Search documentation with Alfred / Dash</a></p></li><li><p><a href="https://tips.rstankov.com/i/145121634/dont-use-tailwind-without-a-component-system">&#128683; Don't use Tailwind without a component system</a></p></li><li><p><a href="https://tips.rstankov.com/i/145121634/how-to-parameterize-components">&#128230; How to parameterize components</a></p><ul><li><p><a href="https://tips.rstankov.com/i/145121634/targeted-props">&#127919; Targeted props</a></p></li><li><p><a href="https://tips.rstankov.com/i/145121634/variants">&#127912; Variants</a></p></li><li><p><a href="https://tips.rstankov.com/i/145121634/margins">&#128207; Margins</a></p></li></ul></li><li><p><a href="https://tips.rstankov.com/i/145121634/writing-custom-css">&#9997;&#65039; Writing custom CSS</a></p></li><li><p><a href="https://tips.rstankov.com/i/145121634/cool-tricks">&#128161; Cool tricks</a></p></li><li><p><a href="https://tips.rstankov.com/i/145121634/tailwind-ui">&#128168; Checkout TailwindUI</a></p></li></ul><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://tips.rstankov.com/subscribe?"><span>Subscribe now</span></a></p><p>So, let's get started &#128073;</p><h2>&#129529;  Use ESLint plugin</h2><p>When used effectively, Tailwind CSS can significantly enhance project structure. One of the initial tools I integrated with Tailwind in my projects was the <a href="https://eslint.org/">ESLint</a> plugin for Tailwind - <a href="https://www.npmjs.com/package/eslint-plugin-tailwindcss">eslint-plugin-tailwindcss</a>.</p><p>Its biggest feature is that it validates class names because making a typo with the Tailwind class names is too easy. Before this plugin, I used a <a href="https://github.com/RStankov/rstankov_com/blob/master/types/tailwind.ts">custom TypeScript checker</a>, which worked well but was clunky.</p><p>A couple of other cool features are:</p><ul><li><p>enforces you to merge multiple classes into one</p></li><li><p>enforces consistent class name order</p></li><li><p>can forbid the usage of arbitrary values</p></li><li><p>avoiding contradicting class names</p></li></ul><p>For VSCode users, there is an <a href="http://The plugin for ESLint">official VSCode plugin</a> as well.</p><h2>&#128270; Search documentation with Alfred / Dash</h2><p>I use <a href="https://kapeli.com/dash">the Dash</a> macOS app for offline documentation and <a href="https://www.alfredapp.com/">Alfred</a> for application launcher.</p><p>There is a plugin that integrates <a href="https://github.com/Kapeli/Dash-Alfred-Workflow">Alfred and Dash</a> that works wonderfully for <a href="https://v2.tailwindcss.com/docs">Tailwind</a>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vdhs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F601642b2-430d-42f1-8037-554a4aca925f_1532x1098.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vdhs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F601642b2-430d-42f1-8037-554a4aca925f_1532x1098.png 424w, https://substackcdn.com/image/fetch/$s_!vdhs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F601642b2-430d-42f1-8037-554a4aca925f_1532x1098.png 848w, https://substackcdn.com/image/fetch/$s_!vdhs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F601642b2-430d-42f1-8037-554a4aca925f_1532x1098.png 1272w, https://substackcdn.com/image/fetch/$s_!vdhs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F601642b2-430d-42f1-8037-554a4aca925f_1532x1098.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vdhs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F601642b2-430d-42f1-8037-554a4aca925f_1532x1098.png" width="1456" height="1044" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/601642b2-430d-42f1-8037-554a4aca925f_1532x1098.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1044,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:635019,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vdhs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F601642b2-430d-42f1-8037-554a4aca925f_1532x1098.png 424w, https://substackcdn.com/image/fetch/$s_!vdhs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F601642b2-430d-42f1-8037-554a4aca925f_1532x1098.png 848w, https://substackcdn.com/image/fetch/$s_!vdhs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F601642b2-430d-42f1-8037-554a4aca925f_1532x1098.png 1272w, https://substackcdn.com/image/fetch/$s_!vdhs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F601642b2-430d-42f1-8037-554a4aca925f_1532x1098.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>It is especially useful when you need to discover if Tailwind supports some CSS properties and what the variants around them are.</p><h2>&#128683; Don't use Tailwind without a component system</h2><p>To avoid messy code and improve maintainability, always pair Tailwind with a component system.</p><p>I don&#8217;t mean a design system but a component system like <a href="https://viewcomponent.org/">ViewComponent</a> or <a href="https://react.dev/">React</a>. Something that allows you to have reusable components.</p><p>Tailwind utility classes are applied only to a single HTML element; if you need to reuse it, you have to copy-paste the code. Then, if you decide to change it, it gets messy.</p><p>A component system is a must when using Tailwind.</p><p>I feel a lot of backlash against Tailwind because people use it as a simple CSS replacement without a component system.</p><h2>&#128230; How to parameterize components</h2><p>I found Tailwind to shine when using a proper component system. Most of your complex and log Tailwind classes are encapsulated in components in this setup. Then, you can use the flex and spacing utils to arrange those components and handle edge cases.</p><p>When we use UI components, we need to be able to configure them.</p><p>Allowing arbitrary class names to be passed around seems very easy.</p><p>This approach can lead to styling conflicts and future issues because the component must know how it&#8217;s configured. I also need help controlling which styles get overwritten. Users won&#8217;t know what is written without checking the component's implementation.</p><p>Here are a couple of techniques I use to avoid allowing class names to be passed on to components.</p><ul><li><p>Targeted props</p></li><li><p>Variants</p></li><li><p>Spacing</p></li></ul><p><em>I'm going to use React as an example. I used those techniques with the <a href="https://viewcomponent.org/">ViewComponent</a> library as well.</em></p><h3>&#127919; Targeted props</h3><p>Here is an example. I have this &#8220;Text&#8221; component, and I want its font size to be adjustable. The naive approach could be the following:</p><pre><code>&lt;Text className="text-sm" /&gt;</code></pre><p>However, I can&#8217;t guarantee that this will be used only for font size or restrict what sizes can be used. </p><p>Plus, how will another developer know what classes to use?</p><p>I can technically restrict TypeScript to allow only &#8220;text-sm&#8221; / &#8220;text-lg&#8221; class names. However, this will break developer expectations, and someone will overwrite when we need another configuration.</p><p>A better approach is to have a separate "targeted" property that only deals with the size.</p><pre><code>&lt;Text /&gt;
&lt;Text size="sm" /&gt;
&lt;Text size="lg" /&gt;</code></pre><p>We can use an object map to define all allowed props. You can even apply different font weights depending on font size.</p><p>Here is the implementation:</p><pre><code>const SIZES = {
  "sm": "text-sm"
  "base": "text-base"
  "lg": "text-lg font-semibold"
}

type IProps = {
  size?: keyof typeof SIZES
}

function Text({ size: "base" }): IProps {
   // ...
}</code></pre><h3>&#127912; Variants</h3><p>"Variants" is an extension of the target props pattern. Instead of targeting a single property, it allows your component to have more distinct variants.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!CRnI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2070836c-71e9-4a6e-92fc-c5b29272f1c8_1546x558.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CRnI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2070836c-71e9-4a6e-92fc-c5b29272f1c8_1546x558.png 424w, https://substackcdn.com/image/fetch/$s_!CRnI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2070836c-71e9-4a6e-92fc-c5b29272f1c8_1546x558.png 848w, https://substackcdn.com/image/fetch/$s_!CRnI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2070836c-71e9-4a6e-92fc-c5b29272f1c8_1546x558.png 1272w, https://substackcdn.com/image/fetch/$s_!CRnI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2070836c-71e9-4a6e-92fc-c5b29272f1c8_1546x558.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!CRnI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2070836c-71e9-4a6e-92fc-c5b29272f1c8_1546x558.png" width="1456" height="526" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2070836c-71e9-4a6e-92fc-c5b29272f1c8_1546x558.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:526,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:65999,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!CRnI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2070836c-71e9-4a6e-92fc-c5b29272f1c8_1546x558.png 424w, https://substackcdn.com/image/fetch/$s_!CRnI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2070836c-71e9-4a6e-92fc-c5b29272f1c8_1546x558.png 848w, https://substackcdn.com/image/fetch/$s_!CRnI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2070836c-71e9-4a6e-92fc-c5b29272f1c8_1546x558.png 1272w, https://substackcdn.com/image/fetch/$s_!CRnI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2070836c-71e9-4a6e-92fc-c5b29272f1c8_1546x558.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Implementation-wise, it is the same as the targeted props&#8212;an object map with variant and class names.</p><p>I have seen two ways to this</p><pre><code>As property

&lt;Button variant="primary"&gt;
&lt;Button variant="secondary"&gt;
&lt;Button variant="small"&gt;

As component

&lt;Button.Primary&gt;
&lt;Button.Secondary&gt;
&lt;Button.Small&gt;</code></pre><p>I prefer the component approach because it allows me to support different props for different variants. Some button variants support icons, and some might not.</p><p>Some components might have both variants and targeted props.</p><h3>&#128207; Margins</h3><p>The other job of having a "className" passed to the component is to set its "margin" relative to some other element on the page. However, it isn't a component's responsibility to know how it is positioned relative to another component; it is the job of the parent component to orchestrate this.</p><p>My approach to this issue is to shift from directly passing "margin" to components. Instead, I advocate for setting properties like &#8220;gap&#8221; and &#8220;padding&#8221; to their parent components or wrapping them in another component. </p><p>This ensures a more efficient and manageable way of handling margins and positioning in web development.</p><p>Here us an example:</p><pre><code>// Bad
&lt;div&gt;
   &lt;MyComponent /&gt;
   &lt;MyComponent className="ml-2" /&gt;
&lt;/div&gt;

// Slightly better
&lt;div&gt;
   &lt;MyComponent /&gt;
   &lt;div className="pl-2"&gt;
     &lt;MyComponent /&gt;
   &lt;/div&gt;
&lt;/div&gt;

// Better
&lt;div className="flex gap-2"&gt;
   &lt;MyComponent /&gt;
   &lt;MyComponent /&gt;
&lt;/div&gt;</code></pre><p>Max Stoiber has a <a href="https://mxstbr.com/thoughts/margin">good article</a> about this subject.</p><h2>&#9997;&#65039; Writing custom CSS</h2><p>interesting CSS.Even though I use Tailwind, I still write CSS for my application. </p><p>Here are my rules for when to write CSS:</p><ul><li><p>What I want doesn&#8217;t have a Tailwind equivalent. Some fancy grid setups and things are dependent on CSS variables.</p></li><li><p>Tailwind classes get too interconnected and hard to reason about. Usually involving styling multiple child components. <em>When the &#8220;Cascading&#8221; part of CSS is needed.</em></p></li><li><p>Automatic style. Style is applied when a data attribute is applied to an HTML element.</p></li><li><p>I want to reuse a class name, not a whole component. An example in <a href="https://angrybuilding.com/">Angry Building</a> is the &#8220;c-input&#8221; class name, which is applied to &#8220;input,&#8221; &#8220;select,&#8221; and &#8220;textarea,&#8221; which are wrapped by different components.</p></li></ul><p>My custom css is</p><ul><li><p>In React projects, I use the <a href="https://github.com/css-modules/css-modules">CSS module</a> in the same folder as the component.</p></li><li><p>In Ruby on Rails projects, prefix all classes with &#8220;c-&#8221;, like &#8220;c-input&#8221; or &#8220;c-button-primary&#8221;.</p></li><li><p>I rely on <a href="https://tailwindcss.com/docs/reusing-styles#extracting-classes-with-apply">@apply</a>, a directive to reuse Tailwind classes in my custom classes. In this way, I don&#8217;t have to remember how much is &#8220;gap-2&#8221; or &#8220;mt-3&#8221; or what is in &#8220;text-sm&#8221;</p></li></ul><p>I don&#8217;t have much custom CSS.</p><h2>&#128161; Cool tricks</h2><p>Tailwind is full of cool tricks. Every time I watch someone writing Tailwind, I learn a lot. One such video is <a href="https://www.youtube.com/watch?v=TNXM4bqGqek">this talk</a> by Adam Wathan (<em>the creator of Tailwind</em>) at Rails World 2023.</p><p>One of my favorite Taildwind features is the "<a href="https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state">group</a>," which allows you to change the child's style based on the parent's actions. This has removed a lot of JS code.</p><pre><code>&lt;div class="group"&gt;
   &lt;span class="group-hover:bg-cloud"&gt;
     When parent element (group) is hovered, I change background
   &lt;/span&gt;
   &lt;span&gt;
     I don't change color
   &lt;/span&gt;
&lt;/div&gt;</code></pre><p>The &#8220;<a href="https://tailwindcss.com/docs/grid-template-columns">grid</a>&#8221; utilizes made the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout">CSS grids</a> a lot more accessible to me. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Gj3o!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53e18a5-b1a1-4f70-8218-92eb9a9ac4b5_1550x554.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Gj3o!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53e18a5-b1a1-4f70-8218-92eb9a9ac4b5_1550x554.png 424w, https://substackcdn.com/image/fetch/$s_!Gj3o!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53e18a5-b1a1-4f70-8218-92eb9a9ac4b5_1550x554.png 848w, https://substackcdn.com/image/fetch/$s_!Gj3o!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53e18a5-b1a1-4f70-8218-92eb9a9ac4b5_1550x554.png 1272w, https://substackcdn.com/image/fetch/$s_!Gj3o!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53e18a5-b1a1-4f70-8218-92eb9a9ac4b5_1550x554.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Gj3o!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53e18a5-b1a1-4f70-8218-92eb9a9ac4b5_1550x554.png" width="1456" height="520" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f53e18a5-b1a1-4f70-8218-92eb9a9ac4b5_1550x554.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:520,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:84641,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Gj3o!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53e18a5-b1a1-4f70-8218-92eb9a9ac4b5_1550x554.png 424w, https://substackcdn.com/image/fetch/$s_!Gj3o!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53e18a5-b1a1-4f70-8218-92eb9a9ac4b5_1550x554.png 848w, https://substackcdn.com/image/fetch/$s_!Gj3o!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53e18a5-b1a1-4f70-8218-92eb9a9ac4b5_1550x554.png 1272w, https://substackcdn.com/image/fetch/$s_!Gj3o!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff53e18a5-b1a1-4f70-8218-92eb9a9ac4b5_1550x554.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>&lt;div class="grid grid-rows-3 grid-flow-col gap-4"&gt;
  &lt;div class="row-span-3 ..."&gt;01&lt;/div&gt;
  &lt;div class="col-span-2 ..."&gt;02&lt;/div&gt;
  &lt;div class="row-span-2 col-span-2 ..."&gt;03&lt;/div&gt;
&lt;/div&gt;</code></pre><h2>&#128168;  Tailwind UI</h2><p><a href="https://tailwindui.com/">Tailwind UI</a> is the official Tailwind CSS components and templates project. It is created and maintained by the team behind Tailwind CSS.</p><p>These are not only great components that can be copied and pasted into a project, but they are also a great learning resource on how to write good Tailwind.</p><h2>Conclusion</h2><p>I have now used Tailwind in Ruby on Rails / ViewComponent and React projects, and I consider Tailwind one of my default tools.</p><p>I still write CSS; however, because of Tailwind, I now only write interesting CSS.</p><p>What I like about Tailwind is</p><ul><li><p>It makes it easy to prototype UIs</p></li><li><p>I can copy <a href="https://tailwindui.com/">Tailwind UI</a> components directly and start iterating on them</p></li><li><p>It keeps my CSS bundle small, and I have to have a single CSS file for my whole project</p></li><li><p>It taught me to think <a href="https://tailwindcss.com/docs/responsive-design#working-mobile-first">mobile-first</a></p></li><li><p>It forces consistency in terms of colors and spacing.</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">If you want to get notified for the next post in the series and my other future posts &#128073;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><p>If you have any tips, you strongly agree or disagree with them or have ideas for something I missed, &#1091;ou can ping me on&nbsp;<a href="https://www.threads.net/@rstankov">Threads</a>,&nbsp;<a href="https://www.linkedin.com/in/radoslavstankov/">LinkedIn</a>,&nbsp;<a href="https://mastodon.social/@rstankov">Mastodon</a>,&nbsp;<a href="https://twitter.com/rstankov">Twitter</a>, or just leave a comment below &#128237;</p>]]></content:encoded></item><item><title><![CDATA[Tips for Dealing With Errors]]></title><description><![CDATA[People always ask how to become a top engineer. One easy trick is to check your production error tracking weekly and fix a couple of errors every week. Engaging with your production error tracker is a valuable learning opportunity. It's a chance to understand how to design systems with fewer errors and develop a sense of ownership.]]></description><link>https://tips.rstankov.com/p/tips-for-dealing-with-errors</link><guid isPermaLink="false">https://tips.rstankov.com/p/tips-for-dealing-with-errors</guid><dc:creator><![CDATA[Radoslav Stankov]]></dc:creator><pubDate>Thu, 23 May 2024 11:58:40 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/222dbdc3-4524-4f69-b72e-d9ded73163cf_1024x1024.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>People always ask how to become a top engineer. One easy trick is to check your production error tracking weekly and fix a couple of errors every week.<br><br>Engaging with your production error tracker is a valuable&nbsp;learning opportunity. It's a chance to understand&nbsp;how to design systems with fewer errors and develop a sense of ownership.</p><p>In the next&nbsp;few&nbsp;posts, I will cover crucial tips and tricks&nbsp;for&nbsp;dealing with run-time exceptions and errors.&nbsp;These insights will be invaluable in your journey to becoming a top engineer.<br></p><p>Today, I&#8217;m going to cover tips about</p><ul><li><p><a href="https://tips.rstankov.com/i/144870793/monitoring">&#128064; Monitoring</a></p></li><li><p><a href="https://tips.rstankov.com/i/144870793/reducing-the-noise">&#128263; Reducing the noise</a></p></li><li><p><a href="https://tips.rstankov.com/i/144870793/fixing-errors">&#128296; Fixing errors</a></p></li><li><p><a href="https://tips.rstankov.com/i/144870793/processes-to-deal-with-errors">&#128203; Processes to deal with errors</a></p></li></ul><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://tips.rstankov.com/subscribe?"><span>Subscribe now</span></a></p><p>So, lets get started &#128073;</p><h2>&#128064; Monitoring</h2><p>The tool I have used the most for error reporting is&nbsp;Sentry. It is one of the first tools I add to every new project I start.</p><p>I often use multiple projects for the same product. For example, at&nbsp;Product Hunt, we had projects for:</p><ul><li><p>[JS] Next.js frontend</p></li><li><p>[Node] Next.js ssr server</p></li><li><p>[Ruby] Ruby on Rails private API and admin&nbsp;</p></li><li><p>[Ruby] Ruby on Rails public API&nbsp;</p></li><li><p>[Ruby] Sidekiq background workers</p></li></ul><p>Even though all [Ruby] projects used the same codebase, they had very different usage patterns and error frequencies.&nbsp;</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2jTZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874627d9-2598-4369-a9a5-a0f4e2703a76_2672x652.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2jTZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874627d9-2598-4369-a9a5-a0f4e2703a76_2672x652.png 424w, https://substackcdn.com/image/fetch/$s_!2jTZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874627d9-2598-4369-a9a5-a0f4e2703a76_2672x652.png 848w, https://substackcdn.com/image/fetch/$s_!2jTZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874627d9-2598-4369-a9a5-a0f4e2703a76_2672x652.png 1272w, https://substackcdn.com/image/fetch/$s_!2jTZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874627d9-2598-4369-a9a5-a0f4e2703a76_2672x652.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2jTZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874627d9-2598-4369-a9a5-a0f4e2703a76_2672x652.png" width="1456" height="355" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/874627d9-2598-4369-a9a5-a0f4e2703a76_2672x652.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:355,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:102416,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!2jTZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874627d9-2598-4369-a9a5-a0f4e2703a76_2672x652.png 424w, https://substackcdn.com/image/fetch/$s_!2jTZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874627d9-2598-4369-a9a5-a0f4e2703a76_2672x652.png 848w, https://substackcdn.com/image/fetch/$s_!2jTZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874627d9-2598-4369-a9a5-a0f4e2703a76_2672x652.png 1272w, https://substackcdn.com/image/fetch/$s_!2jTZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874627d9-2598-4369-a9a5-a0f4e2703a76_2672x652.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Create a dedicated <strong><a href="https://tips.rstankov.com/p/tips-for-using-slack">Slack channel</a></strong> where every new error is logged. <strong>Log all deployments</strong> in this channel, as well to correlate them with any spikes in errors, making it easier to identify and address issues stemming from recent changes.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!08Uy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f0363b7-b75a-4f37-933e-e75fdbd21fdd_1394x1506.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!08Uy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f0363b7-b75a-4f37-933e-e75fdbd21fdd_1394x1506.png 424w, https://substackcdn.com/image/fetch/$s_!08Uy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f0363b7-b75a-4f37-933e-e75fdbd21fdd_1394x1506.png 848w, https://substackcdn.com/image/fetch/$s_!08Uy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f0363b7-b75a-4f37-933e-e75fdbd21fdd_1394x1506.png 1272w, https://substackcdn.com/image/fetch/$s_!08Uy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f0363b7-b75a-4f37-933e-e75fdbd21fdd_1394x1506.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!08Uy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f0363b7-b75a-4f37-933e-e75fdbd21fdd_1394x1506.png" width="520" height="561.7790530846485" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1f0363b7-b75a-4f37-933e-e75fdbd21fdd_1394x1506.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1506,&quot;width&quot;:1394,&quot;resizeWidth&quot;:520,&quot;bytes&quot;:277818,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!08Uy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f0363b7-b75a-4f37-933e-e75fdbd21fdd_1394x1506.png 424w, https://substackcdn.com/image/fetch/$s_!08Uy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f0363b7-b75a-4f37-933e-e75fdbd21fdd_1394x1506.png 848w, https://substackcdn.com/image/fetch/$s_!08Uy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f0363b7-b75a-4f37-933e-e75fdbd21fdd_1394x1506.png 1272w, https://substackcdn.com/image/fetch/$s_!08Uy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f0363b7-b75a-4f37-933e-e75fdbd21fdd_1394x1506.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This channel is a good place to triage the errors and assign them to teammates. Each message can be used as a thread for this particular issue.</p><p>I turn off their email notifications for new errors because if it is serious, it is visible in Slack.</p><p>I keep their &#8220;Weekly Report,&#8221; which is a good overview of the week.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UFFR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb86c42c-872f-4029-be8a-8cfbeb9a7732_1230x1084.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UFFR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb86c42c-872f-4029-be8a-8cfbeb9a7732_1230x1084.png 424w, https://substackcdn.com/image/fetch/$s_!UFFR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb86c42c-872f-4029-be8a-8cfbeb9a7732_1230x1084.png 848w, https://substackcdn.com/image/fetch/$s_!UFFR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb86c42c-872f-4029-be8a-8cfbeb9a7732_1230x1084.png 1272w, https://substackcdn.com/image/fetch/$s_!UFFR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb86c42c-872f-4029-be8a-8cfbeb9a7732_1230x1084.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UFFR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb86c42c-872f-4029-be8a-8cfbeb9a7732_1230x1084.png" width="558" height="491.76585365853657" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/db86c42c-872f-4029-be8a-8cfbeb9a7732_1230x1084.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1084,&quot;width&quot;:1230,&quot;resizeWidth&quot;:558,&quot;bytes&quot;:98648,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!UFFR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb86c42c-872f-4029-be8a-8cfbeb9a7732_1230x1084.png 424w, https://substackcdn.com/image/fetch/$s_!UFFR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb86c42c-872f-4029-be8a-8cfbeb9a7732_1230x1084.png 848w, https://substackcdn.com/image/fetch/$s_!UFFR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb86c42c-872f-4029-be8a-8cfbeb9a7732_1230x1084.png 1272w, https://substackcdn.com/image/fetch/$s_!UFFR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb86c42c-872f-4029-be8a-8cfbeb9a7732_1230x1084.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><blockquote><p>&#128161; The key here is that you or your team shouldn&#8217;t forget to check errors. Sentry should notify you.</p></blockquote><h2>&#128263; Reducing the noise</h2><p>When you start monitoring, the worst that can happen is for people to start ignoring it. This happens because there are too many errors, especially too many unactionable errors. Or, as I like to say, there is &#8220;<em>too much noise</em>&#8221;.</p><p>So, after you have your monitoring, the next step is to reduce the noise.</p><p>You can start reducing noise by muting errors that can&#8217;t be acted upon. Errors like routing errors or errors that containers are shutting down gracefully.</p><p>Here is an example in Ruby on Rails application:</p><pre><code>Sentry.configure do |config|
  config.excluded_exceptions = [
    'Rack::Timeout::RequestExpiryError',        // timeout
    'Rack::Timeout::RequestTimeoutException',   // timeout
    'ActionController::RoutingError',           // 404
    'ActionDispatch::ParamsParser::ParseError', // malformed request
    'Sidekiq::Shutdown',                        // graceful process stop
  ]
end</code></pre><p>You should be very careful here because you don&#8217;t want to start ignoring everything.</p><p>If you are too overwhelmed and know certain parts of your systems get too many errors and you can&#8217;t deal with them, <strong>temporarily</strong> create a Sentry project that acts as a &#8220;<em>junk drawer</em>&#8221; and redirect errors there. When you clean the main project, you can remove the redirect.</p><h2>&#128296; Fixing Errors</h2><p>What should you fix first if you have a lot of errors? My prioritization framework is as follows:</p><ol><li><p>&#128293; Critical errors - ones that cause major damage&nbsp;&nbsp;</p></li><li><p>&#128519; Most easy and noisy&nbsp;</p></li></ol><p>Those "critical errors" are often handled immediately because they bring the whole system down or are very complex and scheduled as part of the regular execution.&nbsp;</p><p>Once the critical errors are under control, the focus shifts to the non-critical ones. The goal here is to reduce noise, so it's best to start with the easiest one (and possibly the most frequent). In this case, quantity matters more than severity.</p><p>This approach ensures that the most disruptive issues are resolved first, followed by those that contribute significantly to the overall error count</p><p>When I don't have a solution for fixing a non-critical error within 20 minutes, I move on to the next error.</p><blockquote><p>&#128161; Fixing errors means fixing errors, not just silencing them by try-catching and ignoring them.&nbsp;</p></blockquote><p>Sometimes, you won't know how to fix an error, but you won't want the user to get a 500 error. In this case, you can catch the error, log it in Sentry (so you are reminded about the core issue), and return a proper error message.</p><p>You want to fix the root causes. Here is an example: the following code throws an error because &#8220;account. subscription&#8221; is null.</p><pre><code>account.subscription.status</code></pre><p>According to your business rules, all accounts should have subscriptions. The &#8220;bad&#8221; fix would be to check for null because this will hide the issue and make it harder to fix later.</p><p>What you should do here:</p><ol><li><p>Check for other accounts without a subscription</p></li><li><p>Find out why those accounts don't have a subscription</p></li><li><p>Fix this</p></li><li><p>Add missing subscriptions to accounts</p></li></ol><blockquote><p>&#128161; Errors are valuable information about your system, don&#8217;t ignore them.</p></blockquote><p>When you start fixing errors in bulk, you will start noticing patterns, leading you to build more resilient code and improve your internal tooling.</p><p>An example is when I noticed many of our background jobs were failing with random network errors, and after a couple of retries, they succeeded. So, I made our background job automatically catch network errors, reschedule, and only log the error in Sentry if the job failed more than 30 times.</p><h2>&#128203; Processes to deal with errors</h2><p>Here are some company processes I have used to deal with errors. From a management point of view, you want to have a process around errors. Otherwise, it is no-man's land.</p><h3>&#127864; Happy Friday</h3><p>This is a process I&#8217;m currently using as a solo developer at <a href="http://Product Hunt">Angry Building</a>. Every Friday, I dedicate time specifically to fixing bugs and addressing errors, ensuring that the system remains stable and any outstanding issues are resolved before the weekend.</p><p>We used this process in the early days at&nbsp;<a href="https://www.producthunt.com/">Product Hunt</a>&nbsp;before we reached 7-8 engineers.</p><p>A developer has dedicated time on Friday to deal with </p><ul><li><p>&#128375; Fix bugs&nbsp;</p></li><li><p>&#128165; Fix exceptions</p></li><li><p>&#128295; Bump dependancies</p></li><li><p>&#128184; Pay technical dept</p></li></ul><h3>&#9876;&#65039; Strike Team</h3><p>As&nbsp;<a href="https://www.producthunt.com/">Product Hunt</a>, it became too chaotic to have this.&nbsp;We have had a dedicated &#8220;Strike team&#8221; consisting of 2 junior developers for some time. They were responsible for Fixing bugs and exceptions, bumping dependencies, and working with our support team.&nbsp;</p><p>Of course, they didn't fix every error; if a recent feature caused errors, the team who worked on it fixed it. Or if the error was too complex, a more senior person paired with them.</p><h3>&#128030; Bug Duty</h3><p>However, when the team crew at&nbsp;<a href="https://www.producthunt.com/">Product Hunt</a>&nbsp;increased to 12+ people, and the "Strike Team" engineers became more senior, we disbanded the team and introduced a "<a href="https://blog.rstankov.com/bug-duty-process/">Bug Duty</a>".&nbsp;&nbsp;</p><p>An engineer was rotated into this role in every sprint. I have written before about this process &#128073;&nbsp;<a href="https://blog.rstankov.com/bug-duty-process/">in my blog</a>.</p><p>I think this worked a lot better than the dedicated "Strike team".&nbsp;</p><h3>&#128027; Bug Bash</h3><p>All previous tips in the post apply when you have a relatively small number of errors. However, if you are flooded, you can try to do "Bug Bash,"&nbsp;which&nbsp;is exclusively focused on fixing exceptions.&nbsp;</p><p>For 1 sprint, most of the team is focused only on fixing exceptions. I suggest making it fun by giving prices and titles like "<em>toughest error</em>", "<em>most errors fixed</em>", and "<em>smartest workaround</em>". &#127942;</p><p>One couple of gotchas about this</p><ul><li><p>Good coordination is needed because multiple people might try to fix the same issue, or one can claim it and then abandon it without releasing it. You can have a Trello board with cards for each error and a limit on "In Progress" so that one developer can only have an error&nbsp;there.</p></li><li><p>A lot of the fixes might be hiding root cause issues by ignoring errors.</p></li><li><p>You need to have proper error setup and followup process otherwise in month or two you will be flooded again</p></li></ul><h2>Conclusion </h2><p>One sign of a stable system without errors. I took a lot of care to have my system running without errors.</p><p>Handling errors is a huge topic, and because of this, I&#8217;m splitting it into a couple of posts.&nbsp;</p><ol><li><p><strong>General tips</strong> (<em>this post</em>)</p></li><li><p>Ruby on Rails tips</p></li><li><p>JavaScript tips</p></li></ol><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">If you want to get notified for the next post in the series and my other future posts &#128073;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>If you have any tips, you strongly agree or disagree with them or have ideas for something I missed, &#1091;ou can ping me on&nbsp;<a href="https://www.threads.net/@rstankov">Threads</a>,&nbsp;<a href="https://www.linkedin.com/in/radoslavstankov/">LinkedIn</a>,&nbsp;<a href="https://mastodon.social/@rstankov">Mastodon</a>,&nbsp;<a href="https://twitter.com/rstankov">Twitter</a>, or just leave a comment below &#128237;</p>]]></content:encoded></item><item><title><![CDATA[Tips for Using Slack]]></title><description><![CDATA[In order to master our craft we need to master our tools. Slack is one of those tools. Here are my tips for Slack.]]></description><link>https://tips.rstankov.com/p/tips-for-using-slack</link><guid isPermaLink="false">https://tips.rstankov.com/p/tips-for-using-slack</guid><dc:creator><![CDATA[Radoslav Stankov]]></dc:creator><pubDate>Thu, 16 May 2024 14:06:10 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/33a8107d-8f04-4a7a-b2fe-60c42eab5443_1024x1024.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In order to master our craft we need to master our tools. When I say tools - I don't mean just our editor, programming language and framework. I include our email client, calendar and etc. Basically everything we use todo our jobs. <a href="https://slack.com/">Slack</a> is one of those tools. A lot of time is spent in <a href="https://slack.com/">Slack</a>. So it is important to use it effectively.</p><p>Today I&#8217;m going to cover tips about Slack. They will be split into "Personal" and "Company" sections. For bonus will list interesting channels I have seen used.</p><ul><li><p>&#128100; <a href="https://tips.rstankov.com/i/144686172/personal-tips">Personal tips</a></p><ul><li><p><a href="https://tips.rstankov.com/i/144686172/post-in-public-channels-instead-of-dms">&#128226; Post in public channels instead of DMs </a></p></li><li><p>&#128172; <a href="https://tips.rstankov.com/i/144686172/use-threads-for-group-discussion">Use threads to group discussion </a></p></li><li><p><a href="https://tips.rstankov.com/i/144686172/responding-with-emojis">&#128515; Responding with emojis </a></p></li><li><p><a href="https://tips.rstankov.com/i/144686172/use-save-for-later-and-reminders">&#9200; Use save for later and reminders </a></p></li><li><p><a href="https://tips.rstankov.com/i/144686172/reduce-the-noise-from-channels">&#128277; Reduce the noise from channels </a></p></li><li><p><a href="https://tips.rstankov.com/i/144686172/profile-and-status">&#128199; Profile and status </a></p></li></ul></li><li><p><a href="https://tips.rstankov.com/i/144686172/company-tips">&#127970; Company tips</a></p><ul><li><p><a href="https://tips.rstankov.com/i/144686172/groups">&#128101; Groups</a></p></li><li><p><a href="https://tips.rstankov.com/i/144686172/naming-channels">&#127991;&#65039; Naming channels</a></p></li><li><p><a href="https://tips.rstankov.com/i/144686172/channels-as-hubs">&#127760; Channels as hubs</a></p></li><li><p><a href="https://tips.rstankov.com/i/144686172/regular-channel-cleanup">&#129529; Regular channel cleanup</a></p></li><li><p><a href="https://tips.rstankov.com/i/144686172/webhooks">&#129693; Webhooks</a></p></li><li><p><a href="https://tips.rstankov.com/i/144686172/interesting-channels">&#127775; Interesting channels </a></p></li></ul></li></ul><p>So, here are my Slack tips &#128073;</p><h2>&#128100; Personal tips</h2><h3>&#128226;  Post in public channels instead of DMs</h3><p>I prefer to post stuff in channels (which might be private channels). This way, many people can participate in the interactions. This is especially important when discussions are for specific projects. You will need to have a lot of channels and good channel hygiene.&nbsp;</p><h3>&#128172; Use threads for group discussion</h3><p>I often hear, "<em>I don't post in channels" because multiple simultaneous makes everything messy</em>". The solution for this is to discuss "<a href="https://slack.com/help/articles/115000769927-Use-threads-to-organize-discussions-">threads</a>". In this way, it is easy to keep track of multiple conversations.</p><p>Slack also includes a section in its UI for tracking updates on threads that interest you. "Activity" &gt; "&#128172; Threads"</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ag2Y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda9eec2c-ab1a-451b-847b-83b47778b2f2_780x770.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ag2Y!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda9eec2c-ab1a-451b-847b-83b47778b2f2_780x770.png 424w, https://substackcdn.com/image/fetch/$s_!ag2Y!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda9eec2c-ab1a-451b-847b-83b47778b2f2_780x770.png 848w, https://substackcdn.com/image/fetch/$s_!ag2Y!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda9eec2c-ab1a-451b-847b-83b47778b2f2_780x770.png 1272w, https://substackcdn.com/image/fetch/$s_!ag2Y!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda9eec2c-ab1a-451b-847b-83b47778b2f2_780x770.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ag2Y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda9eec2c-ab1a-451b-847b-83b47778b2f2_780x770.png" width="484" height="477.79487179487177" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/da9eec2c-ab1a-451b-847b-83b47778b2f2_780x770.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:770,&quot;width&quot;:780,&quot;resizeWidth&quot;:484,&quot;bytes&quot;:115811,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ag2Y!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda9eec2c-ab1a-451b-847b-83b47778b2f2_780x770.png 424w, https://substackcdn.com/image/fetch/$s_!ag2Y!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda9eec2c-ab1a-451b-847b-83b47778b2f2_780x770.png 848w, https://substackcdn.com/image/fetch/$s_!ag2Y!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda9eec2c-ab1a-451b-847b-83b47778b2f2_780x770.png 1272w, https://substackcdn.com/image/fetch/$s_!ag2Y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda9eec2c-ab1a-451b-847b-83b47778b2f2_780x770.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Another useful tip is to summarize a thread once it concludes. Write a summary of the thread with obvious action points and conclusions. I use emojis like - &#9989; and &#128284; to mark the items and post them on the main channel. So people who do not follow the discussion know about important decisions.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AKad!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c933079-2396-4a4e-8157-669c33271c5c_1330x346.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AKad!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c933079-2396-4a4e-8157-669c33271c5c_1330x346.png 424w, https://substackcdn.com/image/fetch/$s_!AKad!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c933079-2396-4a4e-8157-669c33271c5c_1330x346.png 848w, https://substackcdn.com/image/fetch/$s_!AKad!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c933079-2396-4a4e-8157-669c33271c5c_1330x346.png 1272w, https://substackcdn.com/image/fetch/$s_!AKad!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c933079-2396-4a4e-8157-669c33271c5c_1330x346.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AKad!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c933079-2396-4a4e-8157-669c33271c5c_1330x346.png" width="594" height="154.52932330827068" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9c933079-2396-4a4e-8157-669c33271c5c_1330x346.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:346,&quot;width&quot;:1330,&quot;resizeWidth&quot;:594,&quot;bytes&quot;:57860,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!AKad!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c933079-2396-4a4e-8157-669c33271c5c_1330x346.png 424w, https://substackcdn.com/image/fetch/$s_!AKad!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c933079-2396-4a4e-8157-669c33271c5c_1330x346.png 848w, https://substackcdn.com/image/fetch/$s_!AKad!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c933079-2396-4a4e-8157-669c33271c5c_1330x346.png 1272w, https://substackcdn.com/image/fetch/$s_!AKad!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c933079-2396-4a4e-8157-669c33271c5c_1330x346.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h3>&#128515; Responding with emojis</h3><p>Talking about emojis.&nbsp;<em>By reading my posts, you know I used A LOT</em>&nbsp;&#128517;</p><p>I often use emoji reactions to communicate status. I use &#128064; to indicate I'm looking at something; stand by. Or &#9989; When I'm done.</p><p>Here is an example of doing a Pull Request review:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pssD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25e29b71-393a-4b4a-86df-15697a564dba_757x340.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pssD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25e29b71-393a-4b4a-86df-15697a564dba_757x340.png 424w, https://substackcdn.com/image/fetch/$s_!pssD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25e29b71-393a-4b4a-86df-15697a564dba_757x340.png 848w, https://substackcdn.com/image/fetch/$s_!pssD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25e29b71-393a-4b4a-86df-15697a564dba_757x340.png 1272w, https://substackcdn.com/image/fetch/$s_!pssD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25e29b71-393a-4b4a-86df-15697a564dba_757x340.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pssD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25e29b71-393a-4b4a-86df-15697a564dba_757x340.png" width="466" height="209.2998678996037" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/25e29b71-393a-4b4a-86df-15697a564dba_757x340.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:340,&quot;width&quot;:757,&quot;resizeWidth&quot;:466,&quot;bytes&quot;:82994,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!pssD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25e29b71-393a-4b4a-86df-15697a564dba_757x340.png 424w, https://substackcdn.com/image/fetch/$s_!pssD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25e29b71-393a-4b4a-86df-15697a564dba_757x340.png 848w, https://substackcdn.com/image/fetch/$s_!pssD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25e29b71-393a-4b4a-86df-15697a564dba_757x340.png 1272w, https://substackcdn.com/image/fetch/$s_!pssD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25e29b71-393a-4b4a-86df-15697a564dba_757x340.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Sometimes I use &#128078; / &#128077; as a voting system.&nbsp;</p><p>Adding an emoji to a message indicates that you have seen it. Posting something is very irritating and getting zero reactions; you don't know if people saw, forgot, or just agreed.</p><h3>&#9200; Use save for later and reminders</h3><p>You want to be responsive in Slack. However, this can be overwhelming if we get too many messages. I found the Slack "Later" and "Reminder" features useful.&nbsp;</p><p>I used to try to reply to everything and move stuff into Todoist as action items. However, a "fast" response was often too "fast". So I started forcing myself to reply, "I will check this later" (<em>I have a snippet for this</em>) and save it for later with a reminder or not. Reminder depends on how time-sensitive something is.&nbsp;</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4R46!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb28b6aff-338d-4a0f-ae3b-dfe6d9f9fba5_586x258.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4R46!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb28b6aff-338d-4a0f-ae3b-dfe6d9f9fba5_586x258.png 424w, https://substackcdn.com/image/fetch/$s_!4R46!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb28b6aff-338d-4a0f-ae3b-dfe6d9f9fba5_586x258.png 848w, https://substackcdn.com/image/fetch/$s_!4R46!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb28b6aff-338d-4a0f-ae3b-dfe6d9f9fba5_586x258.png 1272w, https://substackcdn.com/image/fetch/$s_!4R46!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb28b6aff-338d-4a0f-ae3b-dfe6d9f9fba5_586x258.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4R46!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb28b6aff-338d-4a0f-ae3b-dfe6d9f9fba5_586x258.png" width="312" height="137.36518771331058" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b28b6aff-338d-4a0f-ae3b-dfe6d9f9fba5_586x258.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:258,&quot;width&quot;:586,&quot;resizeWidth&quot;:312,&quot;bytes&quot;:22187,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4R46!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb28b6aff-338d-4a0f-ae3b-dfe6d9f9fba5_586x258.png 424w, https://substackcdn.com/image/fetch/$s_!4R46!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb28b6aff-338d-4a0f-ae3b-dfe6d9f9fba5_586x258.png 848w, https://substackcdn.com/image/fetch/$s_!4R46!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb28b6aff-338d-4a0f-ae3b-dfe6d9f9fba5_586x258.png 1272w, https://substackcdn.com/image/fetch/$s_!4R46!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb28b6aff-338d-4a0f-ae3b-dfe6d9f9fba5_586x258.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!h_aq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff512f05a-8342-4d9f-8812-7695b157cd75_1190x666.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!h_aq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff512f05a-8342-4d9f-8812-7695b157cd75_1190x666.png 424w, https://substackcdn.com/image/fetch/$s_!h_aq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff512f05a-8342-4d9f-8812-7695b157cd75_1190x666.png 848w, https://substackcdn.com/image/fetch/$s_!h_aq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff512f05a-8342-4d9f-8812-7695b157cd75_1190x666.png 1272w, https://substackcdn.com/image/fetch/$s_!h_aq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff512f05a-8342-4d9f-8812-7695b157cd75_1190x666.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!h_aq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff512f05a-8342-4d9f-8812-7695b157cd75_1190x666.png" width="408" height="228.34285714285716" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f512f05a-8342-4d9f-8812-7695b157cd75_1190x666.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:666,&quot;width&quot;:1190,&quot;resizeWidth&quot;:408,&quot;bytes&quot;:112967,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!h_aq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff512f05a-8342-4d9f-8812-7695b157cd75_1190x666.png 424w, https://substackcdn.com/image/fetch/$s_!h_aq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff512f05a-8342-4d9f-8812-7695b157cd75_1190x666.png 848w, https://substackcdn.com/image/fetch/$s_!h_aq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff512f05a-8342-4d9f-8812-7695b157cd75_1190x666.png 1272w, https://substackcdn.com/image/fetch/$s_!h_aq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff512f05a-8342-4d9f-8812-7695b157cd75_1190x666.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>I have the time of day when I deal to save for later and use the "Slack" todo feature</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qmmR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb3f68c-bfa6-4618-bcdb-eec5f3fca94c_772x696.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qmmR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb3f68c-bfa6-4618-bcdb-eec5f3fca94c_772x696.png 424w, https://substackcdn.com/image/fetch/$s_!qmmR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb3f68c-bfa6-4618-bcdb-eec5f3fca94c_772x696.png 848w, https://substackcdn.com/image/fetch/$s_!qmmR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb3f68c-bfa6-4618-bcdb-eec5f3fca94c_772x696.png 1272w, https://substackcdn.com/image/fetch/$s_!qmmR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb3f68c-bfa6-4618-bcdb-eec5f3fca94c_772x696.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qmmR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb3f68c-bfa6-4618-bcdb-eec5f3fca94c_772x696.png" width="430" height="387.6683937823834" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6eb3f68c-bfa6-4618-bcdb-eec5f3fca94c_772x696.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:696,&quot;width&quot;:772,&quot;resizeWidth&quot;:430,&quot;bytes&quot;:186831,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!qmmR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb3f68c-bfa6-4618-bcdb-eec5f3fca94c_772x696.png 424w, https://substackcdn.com/image/fetch/$s_!qmmR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb3f68c-bfa6-4618-bcdb-eec5f3fca94c_772x696.png 848w, https://substackcdn.com/image/fetch/$s_!qmmR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb3f68c-bfa6-4618-bcdb-eec5f3fca94c_772x696.png 1272w, https://substackcdn.com/image/fetch/$s_!qmmR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb3f68c-bfa6-4618-bcdb-eec5f3fca94c_772x696.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>&#128277; Reduce the noise from channels</h3><p>Slack becomes noisy very fast, and you can be overwhelmed very quickly.&nbsp;&nbsp;</p><p>I'm surprised at how many people need to learn about the&nbsp;<a href="https://slack.com/help/articles/360043207674-Organize-your-sidebar-with-custom-sections">sidebar custom section feature in Slack,</a>&nbsp;Which was essential for me in helping with the organization. When I was at&nbsp;<a href="https://www.producthunt.com/">Product Hunt</a>, I had the following sections</p><ul><li><p>Shortcuts - most used channels for me, those changed frequently depending on the project; I was jumping around</p></li><li><p>Engineering - this grouped every developer in my team</p></li><li><p>Leadership - this grouped everyone on the leadership team and a couple of private channels; we collaborated</p></li><li><p>Channels - all channels I was part of</p></li><li><p>Muted - this was for muted channels, which I was part of but didn't care about.</p></li></ul><p>It looked something like this</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mT-f!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F711d417a-0638-49a9-9bae-9d79e5f652a0_526x384.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mT-f!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F711d417a-0638-49a9-9bae-9d79e5f652a0_526x384.png 424w, https://substackcdn.com/image/fetch/$s_!mT-f!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F711d417a-0638-49a9-9bae-9d79e5f652a0_526x384.png 848w, https://substackcdn.com/image/fetch/$s_!mT-f!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F711d417a-0638-49a9-9bae-9d79e5f652a0_526x384.png 1272w, https://substackcdn.com/image/fetch/$s_!mT-f!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F711d417a-0638-49a9-9bae-9d79e5f652a0_526x384.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mT-f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F711d417a-0638-49a9-9bae-9d79e5f652a0_526x384.png" width="526" height="384" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/711d417a-0638-49a9-9bae-9d79e5f652a0_526x384.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:384,&quot;width&quot;:526,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:96775,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mT-f!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F711d417a-0638-49a9-9bae-9d79e5f652a0_526x384.png 424w, https://substackcdn.com/image/fetch/$s_!mT-f!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F711d417a-0638-49a9-9bae-9d79e5f652a0_526x384.png 848w, https://substackcdn.com/image/fetch/$s_!mT-f!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F711d417a-0638-49a9-9bae-9d79e5f652a0_526x384.png 1272w, https://substackcdn.com/image/fetch/$s_!mT-f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F711d417a-0638-49a9-9bae-9d79e5f652a0_526x384.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>There will be many channels, and you often don't care about them, but you still have to be part of them. For this, I use the Slack notification setting.</p><p>A good way to figure out what channels you should mark with this is to see how often you see a channel in white color in the sidebar and how often you just click to mark it as seen and never read.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Da7-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe99fbe8f-49f0-4ad5-9c30-2f2827cba959_1126x928.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Da7-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe99fbe8f-49f0-4ad5-9c30-2f2827cba959_1126x928.png 424w, https://substackcdn.com/image/fetch/$s_!Da7-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe99fbe8f-49f0-4ad5-9c30-2f2827cba959_1126x928.png 848w, https://substackcdn.com/image/fetch/$s_!Da7-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe99fbe8f-49f0-4ad5-9c30-2f2827cba959_1126x928.png 1272w, https://substackcdn.com/image/fetch/$s_!Da7-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe99fbe8f-49f0-4ad5-9c30-2f2827cba959_1126x928.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Da7-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe99fbe8f-49f0-4ad5-9c30-2f2827cba959_1126x928.png" width="1126" height="928" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e99fbe8f-49f0-4ad5-9c30-2f2827cba959_1126x928.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:928,&quot;width&quot;:1126,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:148024,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Da7-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe99fbe8f-49f0-4ad5-9c30-2f2827cba959_1126x928.png 424w, https://substackcdn.com/image/fetch/$s_!Da7-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe99fbe8f-49f0-4ad5-9c30-2f2827cba959_1126x928.png 848w, https://substackcdn.com/image/fetch/$s_!Da7-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe99fbe8f-49f0-4ad5-9c30-2f2827cba959_1126x928.png 1272w, https://substackcdn.com/image/fetch/$s_!Da7-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe99fbe8f-49f0-4ad5-9c30-2f2827cba959_1126x928.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>&#128199; Profile and status</h3><p>In the onboarding tasks, I give new team members I always give them the following tasks:</p><blockquote><p>1/ Set up working hours.&nbsp;<a href="https://slack.com/help/articles/360025054173-Set-up-Slack-for-work-hours-">Here is how</a>. Because we are a global team, someone will always message you. We don't expect you to answer in the middle of the night.&nbsp;</p><p>2/ Setup your Slack profile with the following data</p><ul><li><p><strong>Profile picture&nbsp;</strong>-<strong> </strong>A photo of you that you feel comfortable sharing, one that will help your coworkers recognize you in a Zoom call.</p></li><li><p><strong>Full name -</strong>&nbsp;Use your real name here.</p></li><li><p><strong>What I do</strong>&nbsp;- This is shown under your picture in Slack. You can enter your official role or anything else to help coworkers understand what you do.</p></li><li><p><strong>Phone number -&nbsp;</strong>Please enter your phone number using the country code. It will not be used unless it&#8217;s an emergency.</p></li><li><p><strong>Time Zone -</strong>&nbsp;It will help Slack and anyone who wants to contact you know the time for you.</p></li><li><p><strong>Role</strong>&nbsp;- Enter your official role as per your role description.</p></li><li><p><strong>Team</strong>&nbsp;- Which team are you a part&nbsp;of</p></li><li><p><strong>Location</strong>&nbsp;- Where in the world do you work from? It's useful since we&#8217;re all around the globe.</p></li></ul><p>3/ Set up&nbsp;<a href="https://slack.com/help/articles/201864558-Set-your-Slack-status-and-availability">Slack status and availability feature</a>. Here are some of the most useful statuses.&nbsp;<em>They will help you communicate why you are not instantly responsible with your team.&nbsp;</em></p><ul><li><p>&#127796; Vacationing</p></li><li><p>&#129298; Out sick</p></li><li><p>&#127919; Focus</p></li><li><p>&#128467;&#65039; In a meeting (<a href="https://slack.com/help/articles/4412365549075-Automations--Sync-your-status-with-your-calendar">here is how to set up</a>)</p></li></ul><p>Of course, you can use fun ones such as &#129327; Solving a hard problem.</p></blockquote><h2>&#127970; Company tips</h2><p>For the last five years, my leadership role has been to "organize" Slack and use it as a company's central hub.&nbsp;</p><h3>&#128101; Groups</h3><p><a href="https://slack.com/help/articles/212906697-Create-a-user-group">Groups</a>&nbsp;allow you to message multiple people at once. Initially, you can start with groups for departments like "@developers" and "@sales". Then you can have subgroups for certain teams like "@platform-team".&nbsp;</p><p>If you have people on call for emergencies, you can create an "@emergancy" group that is notified when emergencies occur. You can rotate people there weekly.</p><h3>&#127991;&#65039; Naming channels</h3><p>We know naming is hard. This is very visible when you open a Slack list of channels, which are often softer by alphabetical order. It looks like a mess &#129526;</p><p>Because of this, I am trying to enforce prefixing Slack channels. Here are some of the prefixes I use:</p><ul><li><p>project-[channel] - project-specific channels, created for just a project or feature</p></li><li><p>[team]-[channel] - channels grouped by a team like engineering, product, or support</p></li></ul><p>Of course, this is hard to enforce and doesn't seem to come naturally &#129335;&#8205;&#9794;&#65039;</p><p>I tried to use "auto-" at one point and separate bot channels from people channels for a long time. But this was a lost battle. You still want to separate channels where humans and bots interact.</p><h3>&#127760; Channels as hubs</h3><p>One of the most efficient and underutilized features in Slack is&nbsp;<a href="https://slack.com/help/articles/205239997-Pin-messages-and-bookmark-links">channel bookmarks</a>. It's a simple yet effective way to keep all the important links to the header of a Slack channel, turning it into a centralized hub for understanding a feature.</p><p>Here is an example of a project channel for a website.&nbsp;</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mdp7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e67f721-2cd5-4734-9956-4d65f2eb6ba0_1064x160.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mdp7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e67f721-2cd5-4734-9956-4d65f2eb6ba0_1064x160.png 424w, https://substackcdn.com/image/fetch/$s_!mdp7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e67f721-2cd5-4734-9956-4d65f2eb6ba0_1064x160.png 848w, https://substackcdn.com/image/fetch/$s_!mdp7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e67f721-2cd5-4734-9956-4d65f2eb6ba0_1064x160.png 1272w, https://substackcdn.com/image/fetch/$s_!mdp7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e67f721-2cd5-4734-9956-4d65f2eb6ba0_1064x160.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mdp7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e67f721-2cd5-4734-9956-4d65f2eb6ba0_1064x160.png" width="1064" height="160" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5e67f721-2cd5-4734-9956-4d65f2eb6ba0_1064x160.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:160,&quot;width&quot;:1064,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:24663,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mdp7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e67f721-2cd5-4734-9956-4d65f2eb6ba0_1064x160.png 424w, https://substackcdn.com/image/fetch/$s_!mdp7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e67f721-2cd5-4734-9956-4d65f2eb6ba0_1064x160.png 848w, https://substackcdn.com/image/fetch/$s_!mdp7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e67f721-2cd5-4734-9956-4d65f2eb6ba0_1064x160.png 1272w, https://substackcdn.com/image/fetch/$s_!mdp7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e67f721-2cd5-4734-9956-4d65f2eb6ba0_1064x160.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>From here, we can see the project status, link to design, and metrics about the site redesign.&nbsp;</p><p><strong>The reality is that work on the project is always split between multiple tools, as many companies want everything in one place. They deal with this with links and a clear path to finding the project data.</strong></p><p>It is also useful for other type of channels. This is an example for the "engineering-emergency" channel:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0kdM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd74ab8d-3614-41c2-bd4a-57ffd248a338_1033x164.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0kdM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd74ab8d-3614-41c2-bd4a-57ffd248a338_1033x164.png 424w, https://substackcdn.com/image/fetch/$s_!0kdM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd74ab8d-3614-41c2-bd4a-57ffd248a338_1033x164.png 848w, https://substackcdn.com/image/fetch/$s_!0kdM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd74ab8d-3614-41c2-bd4a-57ffd248a338_1033x164.png 1272w, https://substackcdn.com/image/fetch/$s_!0kdM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd74ab8d-3614-41c2-bd4a-57ffd248a338_1033x164.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0kdM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd74ab8d-3614-41c2-bd4a-57ffd248a338_1033x164.png" width="1033" height="164" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dd74ab8d-3614-41c2-bd4a-57ffd248a338_1033x164.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:164,&quot;width&quot;:1033,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:19736,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0kdM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd74ab8d-3614-41c2-bd4a-57ffd248a338_1033x164.png 424w, https://substackcdn.com/image/fetch/$s_!0kdM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd74ab8d-3614-41c2-bd4a-57ffd248a338_1033x164.png 848w, https://substackcdn.com/image/fetch/$s_!0kdM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd74ab8d-3614-41c2-bd4a-57ffd248a338_1033x164.png 1272w, https://substackcdn.com/image/fetch/$s_!0kdM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd74ab8d-3614-41c2-bd4a-57ffd248a338_1033x164.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>This&nbsp;links to the <a href="https://tips.rstankov.com/i/142397368/the-emergency-kit">Emergency Kit</a> document, which can guide developers in dealing with emergencies.&nbsp;</p><p>Channels also support to have descriptions. Use this as well.</p><h3>&#129529; Regular channel cleanup</h3><p>Channels are very easy to create and are rarely deleted. Because of this, I had a recurring to-do in the first month of every quarter to go through all channels and archive the unnecessary ones.&nbsp;</p><p>I usually sort channels from newest to oldest because the newest channels are usually reserved for projects or special cases. Following the&nbsp;<a href="https://en.wikipedia.org/wiki/Lindy_effect">Lindy effect</a>.</p><p>A couple of times, I discovered projects that were stuck. Needing a resolution. Like this one A/B test which was "<em>forgotten</em>" &#128584;</p><h3>&#129693; Webhooks</h3><p>Sending automated messages to Slack messages is very easy with&nbsp;<a href="https://api.slack.com/messaging/webhooks">webhooks</a>. </p><p>At&nbsp;<a href="https://angrybuilding.com/">Angry Building</a>, , we send a message to a Slack channel named 'auto-notifications' every time a new user registers or changes plans.</p><h3>&#127775; Interesting channels</h3><p>The heart of Slack organization is channels. Here are some ideas for cool channels I have used. I prefixed with "engineering" because this was my team &#128540;</p><ul><li><p>"engineering-til" - "today, I learn", a place to share interesting learnings from inside and outside the company.&nbsp;<em>This is one of my favorite channels. &#129321;</em></p></li><li><p>"engineering-prs" - a channel to request a pull request reviews. Sometimes, pull request discussions happen in threads there.</p></li><li><p>"engineering-emergency" -&nbsp;<a href="https://tips.rstankov.com/i/142397368/detect-emergencies">when an emergency happens, this is where the team coordinates</a>.</p></li><li><p>"engineering-deploy" - the CI pushes a message to this channel when deployment succeeds or fails.</p></li><li><p>"engineering-sentry" or "engineering-exceptions" - a channel in which every new exception is listed. You want this channel to be silent. It is interesting to coordinate this with "deployment" channels so you can notice when a deployment has a new influx or errors.</p></li><li><p>&#8220;engineering-alert&#8221; - a channel to receive alerts from monitoring systems</p></li><li><p>"engineering-daily" or "engineering-standup" - channel to a&nbsp;<a href="https://en.wikipedia.org/wiki/Stand-up_meeting">virtual standup</a>. It might be automated with something like&nbsp;<a href="https://geekbot.com/">Geekbot</a>.</p></li><li><p>"product-changelog" - a channel where new features are shown to the team.&nbsp;<a href="https://tips.rstankov.com/i/143746817/record-a-video-walkthrough-of-the-feature">I often record videos for PR</a>. I then share those videos on this channel for a video of the features.</p></li><li><p>"product-feedback" or/and "product-ideas" - channel(s) for sharing ideas, feedback, or bugs about the product.</p></li><li><p>"engineering" / "product" / [team] - a catch-all channel for the team. I treat this channel as a big class in code; over time, it accumulates more responsibility, and then it is time to extract a new channel. &#128104;&#8205;&#127891;</p></li><li><p>"kudos" - a channel to celebrate our team members' good work and activity.&nbsp;</p></li><li><p>"fun" or "bs" or "&#8230;" - a channel to fun clips, images and etc.</p></li></ul><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading. Subscribe for free to receive more tips and support my work. &#128588;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>If you have any tips, you strongly agree or disagree with them or have ideas for something I missed, &#1091;ou can ping me on&nbsp;<a href="https://www.threads.net/@rstankov">Threads</a>,&nbsp;<a href="https://www.linkedin.com/in/radoslavstankov/">LinkedIn</a>,&nbsp;<a href="https://mastodon.social/@rstankov">Mastodon</a>,&nbsp;<a href="https://twitter.com/rstankov">Twitter</a>, or just leave a comment below &#128237;</p>]]></content:encoded></item><item><title><![CDATA[Tips for Database Design (Part 2)]]></title><description><![CDATA[Database tips on how to use ActiveRecord more effectively. How to deal with monitoring, namespacing, polymorphic associations, JSON and more...]]></description><link>https://tips.rstankov.com/p/tips-for-database-design-part-2</link><guid isPermaLink="false">https://tips.rstankov.com/p/tips-for-database-design-part-2</guid><dc:creator><![CDATA[Radoslav Stankov]]></dc:creator><pubDate>Wed, 08 May 2024 16:02:17 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/84f5b723-00ae-4c7e-9fbe-6798062b7147_1792x1024.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The database is often the bottleneck in our application, both in terms of performance and developer velocity. Changing the database is a lot harder than changing the code. Because of this, I prefer to defer database structure decisions as late as possible.</p><p>I have mostly worked with <a href="https://www.postgresql.org/">PostgreSQL</a> and <a href="https://rubyonrails.org/">Ruby on Rails</a> <a href="https://guides.rubyonrails.org/active_record_basics.html">ActiveRecord</a> in the last 10-15 years. </p><p>I'm splitting this post on tips into two parts:</p><ol><li><p><a href="https://tips.rstankov.com/p/tips-for-database-design-part-1">General Tips</a></p></li><li><p><strong>Ruby on Rails Tips</strong></p></li></ol><p>This part 2, Ruby on Rails Tips, today I&#8217;m going to cover tips about</p><ul><li><p><a href="https://tips.rstankov.com/i/144436216/annotate-gem">Annotate gem </a></p></li><li><p><a href="https://tips.rstankov.com/i/144436216/database-monitoring-tooling">Database monitoring tooling</a></p></li><li><p><a href="https://tips.rstankov.com/i/144436216/polymorphic-associations">Polymorphic associations</a> </p></li><li><p><a href="https://tips.rstankov.com/i/144436216/namespacing">Namespacing</a></p></li><li><p><a href="https://tips.rstankov.com/i/144436216/handling-race-conditions-and-errors">Handling race conditions and errors</a></p></li><li><p><a href="https://tips.rstankov.com/i/144436216/handling-jsonb-columns">Handling JSONB columns</a></p></li><li><p><a href="https://tips.rstankov.com/i/144436216/useful-gems">Useful gems</a></p></li></ul><p>So, here are my Ruby on Rails tips &#128073;</p><h2>Annotate gem</h2><p>The "<a href="https://rubygems.org/gems/annotate">annotate</a>" gem is a real gem (<em>pun intended</em>) &#129315;</p><p>It is the first gem I have installed in every new project.&nbsp;<em>I would love this to be built into Rails at some point</em>.&nbsp;</p><p>It adds schema information to all Active Record models as a comment. In this way, you don't need to go to `schema.rb` / `structure.sql` or depend on your editor to get this information.&nbsp;</p><p>Having this information at hand has saved me a lot of time. Especially useful part of the annotations is the list of indexes.</p><p>Here is an example:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!MDob!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed63afe1-1a96-48c6-9485-ec6691a2b191_2885x1840.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MDob!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed63afe1-1a96-48c6-9485-ec6691a2b191_2885x1840.png 424w, https://substackcdn.com/image/fetch/$s_!MDob!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed63afe1-1a96-48c6-9485-ec6691a2b191_2885x1840.png 848w, https://substackcdn.com/image/fetch/$s_!MDob!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed63afe1-1a96-48c6-9485-ec6691a2b191_2885x1840.png 1272w, https://substackcdn.com/image/fetch/$s_!MDob!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed63afe1-1a96-48c6-9485-ec6691a2b191_2885x1840.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MDob!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed63afe1-1a96-48c6-9485-ec6691a2b191_2885x1840.png" width="1456" height="929" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ed63afe1-1a96-48c6-9485-ec6691a2b191_2885x1840.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:929,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1481800,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!MDob!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed63afe1-1a96-48c6-9485-ec6691a2b191_2885x1840.png 424w, https://substackcdn.com/image/fetch/$s_!MDob!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed63afe1-1a96-48c6-9485-ec6691a2b191_2885x1840.png 848w, https://substackcdn.com/image/fetch/$s_!MDob!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed63afe1-1a96-48c6-9485-ec6691a2b191_2885x1840.png 1272w, https://substackcdn.com/image/fetch/$s_!MDob!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed63afe1-1a96-48c6-9485-ec6691a2b191_2885x1840.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Database monitoring tooling</h2><p>The database will constantly be the application bottleneck. Good monitoring is essential because the local database has different performance characteristics and data than production.</p><p>A good and free tool is the&nbsp;<a href="https://github.com/ankane/pghero">PGHero</a>&nbsp;gem. It is mounted as a gem and gives the information about:</p><ul><li><p>Duplicated indexes</p></li><li><p>Unused indexes</p></li><li><p>Slow queries</p></li><li><p>Database integrity&nbsp;</p></li><li><p>Database and table sizes</p></li><li><p>How much has the database grown week by week</p></li><li><p>The general health of the database</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Z9HX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2c68be-be91-4f73-84c6-21287d29a316_2400x1596.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Z9HX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2c68be-be91-4f73-84c6-21287d29a316_2400x1596.png 424w, https://substackcdn.com/image/fetch/$s_!Z9HX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2c68be-be91-4f73-84c6-21287d29a316_2400x1596.png 848w, https://substackcdn.com/image/fetch/$s_!Z9HX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2c68be-be91-4f73-84c6-21287d29a316_2400x1596.png 1272w, https://substackcdn.com/image/fetch/$s_!Z9HX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2c68be-be91-4f73-84c6-21287d29a316_2400x1596.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Z9HX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2c68be-be91-4f73-84c6-21287d29a316_2400x1596.png" width="1456" height="968" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cd2c68be-be91-4f73-84c6-21287d29a316_2400x1596.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:968,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:572712,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Z9HX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2c68be-be91-4f73-84c6-21287d29a316_2400x1596.png 424w, https://substackcdn.com/image/fetch/$s_!Z9HX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2c68be-be91-4f73-84c6-21287d29a316_2400x1596.png 848w, https://substackcdn.com/image/fetch/$s_!Z9HX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2c68be-be91-4f73-84c6-21287d29a316_2400x1596.png 1272w, https://substackcdn.com/image/fetch/$s_!Z9HX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2c68be-be91-4f73-84c6-21287d29a316_2400x1596.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>For bigger projects, I reach for&nbsp;<a href="https://pganalyze.com/">pganalyze</a>. I find its&nbsp;<a href="https://pganalyze.com/docs/index-advisor/getting-started">Index Advisor</a>&nbsp;and&nbsp;&nbsp;<a href="https://pganalyze.com/docs/query-performance">Query Performance</a>&nbsp;tools particularly useful. </p><p>The&nbsp;<a href="https://pganalyze.com/docs/query-performance">Query Performance</a>&nbsp;helped me realize I have to&nbsp;<a href="https://tips.rstankov.com/i/144236276/perform-analytics-query-on-a-follower-database-not-primary-one">move the analytics queries out of the main database to a follower replica</a>.</p><h2>Polymorphic associations </h2><p>Polymorphic associations are only a very popular concept in Ruby on Rails. For the JS world, I blame their poor database tooling.</p><p>As with everything they everything, there are trade-offs. The biggest is that you can&#8217;t have foreign keys.</p><p>I find polymorphic association useful for&nbsp;<a href="https://blog.jonathanoliver.com/ddd-strategic-design-core-supporting-and-generic-subdomains/">generic and supporting domain</a>&nbsp;concepts like "comments," "reactions," "notifications," and "logs," which can be attached to or connected to other things in the system.</p><p><a href="https://guides.rubyonrails.org/active_storage_overview.html">ActiveStorage</a>&nbsp;is a great example of how polymorphic associations are useful.</p><p>One issue with default polymorphic behavior in ActiveRecord has 2 drawbacks, in my opinion:</p><ol><li><p>I can't see at one glance what are all possible records for a polymorphic association&nbsp;</p></li><li><p>I can't restrict which records can be the parent of the polymorphic model</p></li></ol><p>Example:</p><pre><code>class Comment &lt; ApplicationRecord
  belongs_to :record, polymorphic: true
end</code></pre><p>What records can have comments?&nbsp;<em>I don't know</em>&nbsp;&#129335;&#8205;&#9794;&#65039;</p><p>For this, I have a "<a href="https://blog.rstankov.com/allowed-class-names-in-activerecord-polymorphic-associations/">belongs_to_polymorphic</a>" helper in my ApplicationRecord:</p><pre><code>class Comment &lt; ApplicationRecord
  belongs_to_polymorphic :record, allowed_classes: [Post, Message, Category, Discussion::Thread]
end</code></pre><p>Now, I can see which records can have comments. Plus, there are some other goodies to this. You can check the full implementation &#128073; <a href="https://gist.github.com/RStankov/098bb71f7c3459f7d3f57fc1b8144c44">here</a>.</p><p>I often add a polymorphic association to a record via&nbsp;<a href="https://api.rubyonrails.org/v7.1.3.2/classes/ActiveSupport/Concern.html">concern</a>.</p><pre><code>module HasComments
  extend ActiveSupport::Concern

  included do
    has_many :comments, as: :record, dependent: :destroy
    has_many :visible_comments, -&gt; { visible }, class_name: 'Comment', inverse_of: :record, as: :record
    has_many :pinned_comments, -&gt; { visible.pinned }, class_name: 'Comment', inverse_of: :record, as: :record

    has_many :commenters, -&gt; { distinct }, through: :visible_comments, source: :user
  end
end</code></pre><p>I use two naming schemes for concerns&nbsp;&nbsp;"-able"&nbsp;(<em>Commentable</em>) and "has" (<em>HasComments</em>).</p><h3>Namespacing</h3><p>I namespace my code very aggressively, including my database models.</p><p>I use a little know Ruby on Rails feature to namespace models in a module -&nbsp;<a href="https://apidock.com/rails/ActiveRecord/Base/table_name_prefix/class">table_name_prefix</a>:</p><pre><code>module Catalog
  def self.table_name_prefix
    'catalog_'
  end
end

# table name becomes "catalog_products"
class Catalog::Product &lt; ApplicationRecord
end


# table name becomes "catalog_categories"
class Catalog::Category &lt; ApplicationRecord
end</code></pre><p>In some situations, it is easy to have a namespace and put all related ActiveRecord models there. For example, in&nbsp;<a href="https://angrybuilding.com/">AngryBuilding</a>, I have namespaces like "Taxation", "BulletinBoard", "Calender" and "Voting". This is the easy case.&nbsp;</p><p>However, sometimes, the whole namespace should be around a single record. Examples from &nbsp;<a href="https://angrybuilding.com/">AngryBuilding</a>&nbsp;are "Building", "Apartment" and "Issue". Those records are a&nbsp;<a href="https://martinfowler.com/bliki/DDD_Aggregate.html">domain aggregate</a>, the root of all other records in the namespace.</p><p>I don't nest ActiveRecord classes inside other ActiveRecord classes. I created a namespace, which is the plural version of this record.&nbsp;</p><p>Here is an example:</p><pre><code>class Building &lt; ApplicationRecord
  # NOTICE: those are not 'building_reports` or `building_manager`
  has_many :reports, class_name: 'Buildings::Report'
  has_many :managers, class_name: 'Buildings::Manager'
end

# separate namespace from root record
module Buildings
  def self.table_name_prefix
    'building_'
  end
end

class Buildings::Report &lt; ApplicationRecord
  belongs_to :building
end

class Buildings::Manager &lt; ApplicationRecord
  belongs_to :building
end</code></pre><h3>Handling race conditions and errors</h3><p>Let's say you have an application like&nbsp;<a href="https://www.producthunt.com/">ProductHunt</a>&nbsp;where users can vote on posts. One user can vote only once on a post.&nbsp;</p><p>The way to ensure this is with a UNIQUE index in votes tables on the user_id and post_id columns. Then, in your application, you have code like:</p><pre><code>Vote.find_or_create_by!(user: user, post: post)</code></pre><p>Then you start noticing in your exception tracker that many requests fail with `PG::UniqueViolation`. </p><p>The reason for this is that 2 requests hit the code at the same time. Both find the return nil and try to create a vote. The first one succeeds, and the second one fails.&nbsp;</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0Lfn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98be63ab-e74f-401d-a509-76a67d6cb9d4_1860x452.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0Lfn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98be63ab-e74f-401d-a509-76a67d6cb9d4_1860x452.png 424w, https://substackcdn.com/image/fetch/$s_!0Lfn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98be63ab-e74f-401d-a509-76a67d6cb9d4_1860x452.png 848w, https://substackcdn.com/image/fetch/$s_!0Lfn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98be63ab-e74f-401d-a509-76a67d6cb9d4_1860x452.png 1272w, https://substackcdn.com/image/fetch/$s_!0Lfn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98be63ab-e74f-401d-a509-76a67d6cb9d4_1860x452.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0Lfn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98be63ab-e74f-401d-a509-76a67d6cb9d4_1860x452.png" width="1456" height="354" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/98be63ab-e74f-401d-a509-76a67d6cb9d4_1860x452.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:354,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:68757,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0Lfn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98be63ab-e74f-401d-a509-76a67d6cb9d4_1860x452.png 424w, https://substackcdn.com/image/fetch/$s_!0Lfn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98be63ab-e74f-401d-a509-76a67d6cb9d4_1860x452.png 848w, https://substackcdn.com/image/fetch/$s_!0Lfn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98be63ab-e74f-401d-a509-76a67d6cb9d4_1860x452.png 1272w, https://substackcdn.com/image/fetch/$s_!0Lfn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98be63ab-e74f-401d-a509-76a67d6cb9d4_1860x452.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The solution for this is this utility, I copy from project to project:</p><pre><code>AngrySupport::Handle::RaceCondition.call do
  Vote.find_or_create_by!(user: user, post: post)
end</code></pre><p>It catches the errors and reply. You can see the <a href="https://gist.github.com/RStankov/098bb71f7c3459f7d3f57fc1b8144c44#file-handle_race_coditions-rb">code in this &#128073; gist</a>.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6rXG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ad50b07-61bc-489d-81f5-8747f5035d22_2392x386.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6rXG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ad50b07-61bc-489d-81f5-8747f5035d22_2392x386.png 424w, https://substackcdn.com/image/fetch/$s_!6rXG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ad50b07-61bc-489d-81f5-8747f5035d22_2392x386.png 848w, https://substackcdn.com/image/fetch/$s_!6rXG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ad50b07-61bc-489d-81f5-8747f5035d22_2392x386.png 1272w, https://substackcdn.com/image/fetch/$s_!6rXG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ad50b07-61bc-489d-81f5-8747f5035d22_2392x386.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6rXG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ad50b07-61bc-489d-81f5-8747f5035d22_2392x386.png" width="1456" height="235" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4ad50b07-61bc-489d-81f5-8747f5035d22_2392x386.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:235,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:137773,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6rXG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ad50b07-61bc-489d-81f5-8747f5035d22_2392x386.png 424w, https://substackcdn.com/image/fetch/$s_!6rXG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ad50b07-61bc-489d-81f5-8747f5035d22_2392x386.png 848w, https://substackcdn.com/image/fetch/$s_!6rXG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ad50b07-61bc-489d-81f5-8747f5035d22_2392x386.png 1272w, https://substackcdn.com/image/fetch/$s_!6rXG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ad50b07-61bc-489d-81f5-8747f5035d22_2392x386.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p></p><p>Another group of race-condition errors I have seen is the "PG::TRDeadlockDetected" in&nbsp;<a href="https://guides.rubyonrails.org/active_job_basics.html">ActiveJob</a>&nbsp;workers.</p><p>I handle this with a helper in my base "ApplicationJob" class:</p><pre><code>class ApplicationJob &lt; ActiveJob::Base
  include AngrySupport::Handle::Job::DatabaseErrors
end</code></pre><p>You can see the code in this &#128073; <a href="https://gist.github.com/RStankov/098bb71f7c3459f7d3f57fc1b8144c44#file-handle_job_database_errors-rb">gist</a>.</p><h2>Handling JSONB columns</h2><p>JSONB columns are very useful. They have similar issues as polymorphic associations. You often have to guess what the structure of the JSONB column content is.</p><p>For this, the "<a href="https://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html">ActiveRecord::Attributes</a>" API comes to the rescue. It allows us to map JSONB columns to Ruby classes; this way, we don't have to deal with hashes.&nbsp;</p><p>Here is an example where we use &#8220;CustomValue&#8221; class to represent a JSONB column.</p><p>First we have to define an new type</p><pre><code>class CustomValueType &lt; ActiveRecord::Type::Value
  def type
    :jsonb
  end

  def cast(value)
    case value
    in CustomValue then value
    in Hash then CustomValue.from_json(value)
    end
  end

  def deserialize(value)
    CustomValue.from_json(ActiveSupport::JSON.decode(value))
  end

  def serialize(value)
    value.to_json
  end
end</code></pre><p>Then we can use this type in a record.</p><pre><code>class MyRecord &lt; ApplicationRecord
  attribute :custom, CustomValueType.new
end</code></pre><p>This works not only for JSONB columns; you can use, for example, a string column to represent a custom month class - "2024-05"&nbsp; &#128519;</p><h2>Useful gems</h2><p>The Ruby on Rails ecosystem, which is related to databases, is rich. The Active Record itself includes a lot of batteries already &#128267;</p><p>Here are some of the gems I use regularly.&nbsp;</p><ul><li><p><a href="https://rubygems.org/gems/annotate">annotate</a>&nbsp;- Puts database structure as a comment in your models&nbsp;</p></li><li><p><a href="https://github.com/ankane/pghero">pghero</a>&nbsp;- A performance dashboard engine</p></li><li><p><a href="https://github.com/magnusvk/counter_culture">counter_culture</a>&nbsp;- Enhances counter caches.&nbsp;<a href="https://tips.rstankov.com/i/143262911/tips-for-using-counter-caches-with-active-record">I have a full post about it &#128073; here</a>.</p></li><li><p><a href="https://github.com/ankane/strong_migrations">strong_migrations</a>&nbsp;- Catch unsafe migrations in development</p></li><li><p><a href="https://github.com/toptal/database_validations">database_validations</a>&nbsp;- Moves validations from Ruby to database</p></li><li><p><a href="https://rubygems.org/gems/blazer">blazer</a>&nbsp;- Business Intelligence tool as engine, allows you to write SQL and create dashboards inside your app</p></li></ul><h2>Conclusion </h2><p>This concludes my tips for this week. &#128517;</p><p>ActiveRecord is one of the best and most magical parts of Rails. However, it is too easy to shoot yourself in the foot. Often, it is not even ActiveRecord's fault when we have bad database design, for example.</p><p>It would help if you had everything in front of you so you don't have to go around gathering information.&nbsp;&nbsp;</p><p>My "belongs_to_polymorphic" helper, "ActiveRecord::Type::Value", and "annotate" gem solve for this.</p><p>If you haven't checked part 1, you can check it &#128073;&nbsp;<a href="https://tips.rstankov.com/p/tips-for-database-design-part-1">here</a>.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading. Subscribe for free to receive more tips and support my work. &#128588;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>If you have any tips, you strongly agree or disagree with them or have ideas for something I missed, &#1091;ou can ping me on&nbsp;<a href="https://www.threads.net/@rstankov">Threads</a>,&nbsp;<a href="https://www.linkedin.com/in/radoslavstankov/">LinkedIn</a>,&nbsp;<a href="https://mastodon.social/@rstankov">Mastodon</a>,&nbsp;<a href="https://twitter.com/rstankov">Twitter</a>, or just leave a comment below &#128237;</p>]]></content:encoded></item><item><title><![CDATA[Tips for Database Design (Part 1)]]></title><description><![CDATA[The database is often the bottleneck in our application, both in terms of performance and developer velocity. Changing the database is a lot harder than changing the code.]]></description><link>https://tips.rstankov.com/p/tips-for-database-design-part-1</link><guid isPermaLink="false">https://tips.rstankov.com/p/tips-for-database-design-part-1</guid><dc:creator><![CDATA[Radoslav Stankov]]></dc:creator><pubDate>Thu, 02 May 2024 16:41:47 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/5a2fdfc1-57f9-42b8-a5e2-8bce96258d97_1792x1024.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The database is often the bottleneck in our application, both in terms of performance and developer velocity. Changing the database is a lot harder than changing the code. Because of this, I prefer to defer database structure decisions as late as possible.</p><p>I have mostly worked with <a href="https://www.postgresql.org/">PostgreSQL</a> and <a href="https://rubyonrails.org/">Ruby on Rails</a> <a href="https://guides.rubyonrails.org/active_record_basics.html">ActiveRecord</a> in the last 10-15 years. </p><p>I'm splitting this post on tips into two parts:</p><ol><li><p><strong>General Tips</strong></p></li><li><p><a href="https://tips.rstankov.com/p/tips-for-database-design-part-2">Ruby on Rails Tips</a></p></li></ol><p>Tips, I&#8217;m going to cover today:</p><ul><li><p><a href="https://tips.rstankov.com/i/144236276/primary-keys">Primary keys</a></p></li><li><p><a href="https://tips.rstankov.com/i/144236276/keys-and-indexes">Keys and indexes</a></p></li><li><p><a href="https://tips.rstankov.com/i/144236276/naming">Naming</a></p></li><li><p><a href="https://tips.rstankov.com/i/144236276/avoid-nulls">How to avoid nulls</a></p></li><li><p><a href="https://tips.rstankov.com/i/144236276/enums">Dealing with Enums</a></p></li><li><p><a href="https://tips.rstankov.com/i/144236276/time-as-boolean">Use time as boolean</a></p></li><li><p><a href="https://tips.rstankov.com/i/144236276/avoid-soft-delete">Avoid soft delete</a></p></li><li><p><a href="https://tips.rstankov.com/i/144236276/record-of-who-did-it">Record who did it</a></p></li><li><p><a href="https://tips.rstankov.com/i/144236276/split-data-out-of-tables">Split data into multiple tables</a></p></li><li><p><a href="http://Perform analytics query on a follower database, not primary one">Analytics vs application queries</a></p></li></ul><p>So, here are the general tips &#128073;</p><h2>Primary keys</h2><p>Every table should have an "id" column for the primary key, which should be "bigint" or "UUID." I usually<em>&nbsp;use "bigint."</em></p><p><strong>NEVER</strong>&nbsp;use it as a domain value for the primary key of a table like email, IBAN, company identifier, etc. </p><ul><li><p>Data is not immutable -&nbsp;<em>people can change emails</em>.</p></li><li><p>Can leak private data -&nbsp;<em>user email shouldn&#8217;t be in the "users/:id" URL</em></p></li><li><p>It can block the making of business rules and changes.&nbsp;&nbsp;</p><ul><li><p>I know about a company that used BULSTAT (company identifier) as the primary key of their accounts table, and this blocked them from supporting individual accounts because individuals don't have BULSTAT. &#129318;&#8205;&#9794;&#65039;</p></li></ul></li></ul><h2>Keys and indexes</h2><p>Always have a foreign key. Except in cases where the table is for logging purposes, and data should be retained even when the related record is deleted. &#128272;</p><p>In PostgreSQL, when there is a foreign key, and index isn't automatically generated. Indexes are created separately.</p><p>The big difference between an index and a foreign key is that a foreign key ensures that a column from one table matches another. For example, "posts.user_id" matches the "users. id" column. However, foreign keys don't make queries faster; indexes are used to improve performance.</p><p>Use compound indexes (index on multiple columns). If applicable, also add unique indexes.&nbsp;</p><p>If there are indexes on "user_id" and "user_id, slug," for example, the "user_id" index isn't needed because the second index covers it. Order matters - "slug, user_id" doesn't use an index search on "user_id".</p><p>When having a "<a href="https://api.rubyonrails.org/v7.1.3.2/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_one">has_one</a>" relationship, use the UNIQUE index.</p><h2>Naming</h2><p>I follow the following naming conventions.</p><ul><li><p>Date columns end with "_date"</p></li><li><p>Times columns end with "_at"&nbsp;</p></li><li><p>Boolean columns don't start with "is_" unless I'm using Ruby on Rails; otherwise, they start with "is_" / "are_"</p></li><li><p>Suffix columns with "_eid" for external system references, like "stripe_subscription_eid"</p></li><li><p>Suffix tables with "_log" for write-only logging tables, like "sign_in_attempts_logs"</p></li><li><p>Use "kind" instead of "type" because Ruby on Rails uses a type of&nbsp;<a href="https://thoughtbot.com/ruby-science/single-table-inheritance-sti.html">Single Table Inheritance</a></p></li><li><p>Use "record" (record_type/record_id) for&nbsp;<a href="https://guides.rubyonrails.org/association_basics.html#polymorphic-associations">polymorphic associations</a>&nbsp;when I can't find a better domain name</p></li><li><p>Use &#8220;source&#8221; as a term when one record is responsible for creating another. An example transactions table can have a source which will be what triggered the transaction</p></li></ul><h2>Avoid nulls</h2><p>I always try to force "NOT NULL" as much as possible.&nbsp;</p><p>Number columns, for example, are always "NOT NULL DEFAULT 0" by default.</p><p>Boolean columns should always be "NOT NULL" and, when possible, have a default value.&nbsp;</p><p>Sometimes, you may be tempted to use null for a boolean, like when you have a new notification type and want null to indicate that the user hasn't decided on this option. There are better ways to handle this, like:</p><ul><li><p>Have an enum with "default," "on," and "off" values. This enables adding possible values like "always" or "only important ones" in the future.</p></li><li><p>We have a separate table, "notification_preferences," with columns "name" and "enabled." The user hasn't picked a value if there is no notification record. This allows new notification types to be added without changing the database.</p></li></ul><p>Often, wanting not to have null pushes me to have a better design.&nbsp; &#129321;</p><h2>Enums</h2><p>I prefer strings for enum columns instead of the enum type in PostgreSQL.</p><p>This helps me avoid:&nbsp;</p><ul><li><p>I don't have to run a database migration when I add new enum values.&nbsp;<em>Changing code is easier than changing the database.</em></p></li><li><p>I don't have to think about how to name the enum type.&nbsp;<em>Naming is hard</em>.</p></li><li><p>I'm not tempted to share enum between tables because enum values look similar.&nbsp;</p></li></ul><p><em>(I know)</em>&nbsp;I'm missing some performance gains and database constraints.&nbsp;When needed, I use the enum type;&nbsp;however, this was needed very rarely.</p><p><a href="https://api.rubyonrails.org/v5.1/classes/ActiveRecord/Enum.html">Rails enums</a>, by default, are numbers - don't do this! Use the&nbsp;<a href="https://hsps.in/post/using-string-as-active-record-enum/">strings</a>:</p><pre><code>enum status: { pending: 'pending', sent: 'sent', failed: 'failed' }</code></pre><p>Having numbers for enum can lead to "<em>fun</em>" situations.</p><p>Example: You have user roles: operator(1), supervisor(2), admin(3). Then you need a director role, which should be between supervisor and admin. <em>What number will you use</em>? &#129300;</p><ul><li><p>If 4. It will be weird that 4 roles have fewer privileges than 3. Plus, the code compares roles for permission. This will mess this up.&nbsp;</p></li><li><p>If 3. Then move admin to 100 (because you want to avoid getting into this situation again). This will require costly migration and changes in the application code.</p></li></ul><p>When you write an analytics query, you will waste time remembering what "4" stands for.  &#129760;</p><h2>Time as boolean</h2><p>Instead of having a boolean column, use a nullable timestamp column.</p><p>Example: Marking an apartment as archived. There are two options:</p><ul><li><p>Boolean column - "archived BOOLEAN NOT NULL DEFAULT 'FALSE'"</p></li><li><p>&nbsp;Timestamp column "archived_at TIMESTAMP WITHOUT TIME ZONE"&nbsp;</p></li></ul><p>I often will like to know when record was archived, so keep this as column "archived_at".&nbsp;</p><p>Then, I have an index:</p><p>```</p><p>CREATE INDEX index_apartments_on_archived_at ON public.apartments USING btree (archived_at) WHERE (archived_at IS NOT NULL);</p><p>```</p><p>This technique is helpful when a state needs to be changed at a certain time. Example: "posts.published_at"</p><p><strong>Not every boolean should be a timestamp.&nbsp;</strong>My question when deciding is, "Do I care when this happened?". &#9200;</p><h3>Avoid soft-delete</h3><p><em>This one might be a bit controversial</em>. &#128517;</p><p>Adding the "delete_at" column sounds very simple. But it rarely is.&nbsp;</p><p>When we haven't <a href="https://en.wiktionary.org/wiki/soft_deletion">soft-deleted</a> as a concern from day one, all the queries aren't expected to be filtered by this column.&nbsp;</p><p>When we soft-delete a record, we need to soft-delete everything related to it. So, one soft-deletable record makes everything around it soft-deletable.</p><p>Plus, we must upgrade the "deleted" records when we make database changes.</p><p>I often prefer a "deleted records" table where I copy data from the deleted records. I use the [auditable](https://rubygems.org/gems/auditable) for this.</p><p>The question always should be, why do we need deleted records to be around? How often are we going to restore deleted records?&nbsp;</p><p>It is always about context and tradeoffs.</p><p>Sometimes, a "soft delete" is needed and can't be avoided. In this case,&nbsp;try to avoid its blast radius.</p><p>This is a good post about this subject &#128073;&nbsp;<a href="https://brandur.org/soft-deletion">Soft Deletion Probably Isn't Worth It</a>.</p><h2>Record of who did it</h2><p>Often, I need a record of who performed a certain action, together with some extra information like location, browser information, and so on.</p><p>I&nbsp;used to attach&nbsp;this information to the table showing what was acted on.&nbsp;</p><ul><li><p>The table becomes too big - with a lot of columns, which are often unnecessary when selecting data</p></li><li><p>Data loss - there is only a record of the last performed action and who did it</p></li><li><p>Explosion of foreign keys/indexes to the "users" table</p></li></ul><p>A better strategy is to have a separate table for user actions, something like the following:</p><pre><code>CREATE TABLE user_action_logs (
    id bigint NOT NULL,
    record_type string NOT NULL,
    record_id bigint NOT NULL,
    user_id bigint NOT NULL,
   action_name string,
    action_info jsonb,
    request_country string,
    request_ip string,
    request_user_agent string,
    created_at timestamp without time zone NOT NULL,
    updated_at timestamp without time zone&nbsp;NOT NULL
);</code></pre><h2>Split data out of tables&nbsp;</h2><p>When there is a big table, it attracts more and more columns. It is like a black hole. Those tables are usually the "<a href="https://en.wikipedia.org/wiki/God_object">Got object</a>" tables in the project. &#9986;&#65039;</p><p>I&#8217;m always very careful when I need to add new columns to those tables.. In many cases, I prefer to have separate tables. Having separate tables is great because they are another bucket to which I can attach things.&nbsp;</p><p>Often, those tables start as a "<a href="https://api.rubyonrails.org/v7.1.3.2/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_one">has_one</a>" relationship to their parent table.</p><h2>Perform analytics query on a follower database, not primary one</h2><p>The analytics queries are very different than application queries.&nbsp; </p><p>The application queries must be fast, select only a small subset of data, and avoid aggregations.</p><p>The analytics queries are the exact opposite&#8212;they use a lot of aggregations, select a larger set of data, and can be slow.</p><p>Usually, the database schema is optimized for application queries.&nbsp;</p><p>It is a mistake to run those different types of queries in the same database instance.&nbsp;</p><p>If you can't move your data to a separate analytics database like&nbsp;<a href="https://aws.amazon.com/redshift/">Amazon Redshift</a>&nbsp;and&nbsp;<a href="https://www.snowflake.com/en/">Snowflake</a>. Second best choice is to perform analytics database queries on <a href="https://en.wikipedia.org/wiki/Master%E2%80%93slave_(technology)">a follower database instance</a>. This instance can be smaller and cheaper than the primary database.</p><p>At&nbsp;<a href="https://www.producthunt.com/">Product Hunt</a>, we had a follower database just for analytics. Then, we moved our daily newsletter there because newsletter generation was slowing the whole website. We later opted out of the delivery, but this was a nice temporary fix.</p><h2>Conclusion</h2><p>This is the end of part 1.&nbsp;</p><p>When I design a new feature, I put extra care in my database design. Database changes are one of the hardest migrations you need to make occasionally.</p><p>Next week, the next part should focus on tips on&nbsp;<a href="https://rubyonrails.org/">Ruby on Rails</a>&nbsp;and databases.&nbsp;<em>I had originally tried to list them here, but they made the post too long &#128517;</em></p><p><strong>Update: <a href="https://tips.rstankov.com/p/tips-for-database-design-part-2">Part 2 is published</a></strong></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading. Subscribe for free to receive more tips and support my work. &#128588;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>If you have any tips, you strongly agree or disagree with them or have ideas for something I missed, &#1091;ou can ping me on&nbsp;<a href="https://www.threads.net/@rstankov">Threads</a>,&nbsp;<a href="https://www.linkedin.com/in/radoslavstankov/">LinkedIn</a>,&nbsp;<a href="https://mastodon.social/@rstankov">Mastodon</a>,&nbsp;<a href="https://twitter.com/rstankov">Twitter</a>, or just leave a comment below &#128237;</p>]]></content:encoded></item><item><title><![CDATA[Tips for Using ViewComponents in Rails]]></title><description><![CDATA[How to simplify your Rails view layer with ViewComponents. Practical tips and examples.]]></description><link>https://tips.rstankov.com/p/tips-for-using-viewcomponents-in</link><guid isPermaLink="false">https://tips.rstankov.com/p/tips-for-using-viewcomponents-in</guid><dc:creator><![CDATA[Radoslav Stankov]]></dc:creator><pubDate>Thu, 25 Apr 2024 12:58:52 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/1ac17a63-9e75-45de-8057-5ce9382047c0_1792x1024.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One of the concepts that needs to be added in Ruby on Rails is the concept of UI components. We have <a href="https://guides.rubyonrails.org/action_view_helpers.html">helpers</a> and <a href="https://guides.rubyonrails.org/layouts_and_rendering.html#using-partials">partials</a>. However, as the project grows, it becomes very, very messy. Plus, partials are <a href="https://scoutapm.com/blog/performance-impact-of-using-ruby-on-rails-view-partials">surpassingly</a> slow. &#129765;</p><p>To solve this problem at <a href="https://angrybuilding.com/">Angry Building</a>, I'm using <a href="https://viewcomponent.org/">ViewComponent</a> gem from <a href="https://github.com/">Github</a>.</p><p>I&#8217;m going to cover today</p><ol><li><p><a href="https://tips.rstankov.com/i/143985579/what-is-viewcomponent">What is ViewComponent</a></p></li><li><p><a href="https://tips.rstankov.com/i/143985579/when-should-i-use-viewcomponent">When to use ViewComponent</a></p></li><li><p><a href="https://tips.rstankov.com/i/143985579/tips">Tips about using ViewComponent</a></p></li><li><p><a href="https://tips.rstankov.com/i/143985579/the-builder-pattern">The builder pattern</a></p></li></ol><p><em>I gave a talk about ViewComponent (<a href="https://speakerdeck.com/rstankov/component-driven-ui-with-viewcomponent-gem">slides</a> and <a href="https://www.youtube.com/watch?v=JlksEZMXt8Y">video</a>). This post is largely based on this post.</em></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Rado's Tips! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><h2>What is ViewComponent?</h2><p><a href="https://viewcomponent.org/">ViewComponent</a>&nbsp;is a gem that allows you to create UI components encapsulated in Ruby classes. It was extracted from&nbsp;<a href="https://github.com/">Github</a>&nbsp;and is also used by&nbsp;<a href="https://gitlab.com/">Gitlab</a>.</p><p>Here is an example of &#8220;FieldsetComponent".</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Aspy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d32e74-5a06-4260-b9aa-97ef9d0ec411_2394x2374.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Aspy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d32e74-5a06-4260-b9aa-97ef9d0ec411_2394x2374.png 424w, https://substackcdn.com/image/fetch/$s_!Aspy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d32e74-5a06-4260-b9aa-97ef9d0ec411_2394x2374.png 848w, https://substackcdn.com/image/fetch/$s_!Aspy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d32e74-5a06-4260-b9aa-97ef9d0ec411_2394x2374.png 1272w, https://substackcdn.com/image/fetch/$s_!Aspy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d32e74-5a06-4260-b9aa-97ef9d0ec411_2394x2374.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Aspy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d32e74-5a06-4260-b9aa-97ef9d0ec411_2394x2374.png" width="266" height="263.8076923076923" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/89d32e74-5a06-4260-b9aa-97ef9d0ec411_2394x2374.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1444,&quot;width&quot;:1456,&quot;resizeWidth&quot;:266,&quot;bytes&quot;:1431658,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Aspy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d32e74-5a06-4260-b9aa-97ef9d0ec411_2394x2374.png 424w, https://substackcdn.com/image/fetch/$s_!Aspy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d32e74-5a06-4260-b9aa-97ef9d0ec411_2394x2374.png 848w, https://substackcdn.com/image/fetch/$s_!Aspy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d32e74-5a06-4260-b9aa-97ef9d0ec411_2394x2374.png 1272w, https://substackcdn.com/image/fetch/$s_!Aspy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d32e74-5a06-4260-b9aa-97ef9d0ec411_2394x2374.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This is how this component is going to be rendered:</p><pre><code>&lt;%= render FieldsetComponent.new(fieldset, title: "Bank account") do %&gt;
  &lt;%= form.input :bank %&gt;
  &lt;%= form.input :iban %&gt;
  &lt;%= form.input :bic %&gt;
&lt;% end %&gt;</code></pre><p>The component has two parts&#8212;a Ruby class and an ERB template</p><pre><code># app/components/fieldset_component.rb
class FieldsetComponent &lt; ViewComponent::Base
  attr_reader :title

  def initialize(title:)
    @title = title
  end
end</code></pre><pre><code>&lt;!-- app/components/fieldset_component.html.erb --&gt;
&lt;fieldset class="c-box p-4"&gt;
  &lt;legend class="c-box px-3 py-1"&gt;
    &lt;%= title %&gt;
  &lt;/legend&gt;
  &lt;div class="space-y-6"&gt;
    &lt;%= content %&gt;
  &lt;/div&gt;
&lt;/fieldset&gt;</code></pre><p>&#8230;and that's it. Pretty simple &#129321;</p><p>The&nbsp;<a href="https://viewcomponent.org/">official site</a>&nbsp;of&nbsp;<a href="https://viewcomponent.org/">ViewComponent</a>&nbsp;has excellent documentation.&nbsp;</p><p>Consider integrating&nbsp;<a href="https://lookbook.build/">Lookbook</a>&nbsp;with ViewComponent. Its setup is just a "gem install" and adding a route.</p><p>It can't even be compared to the complexity of integrating&nbsp;<a href="https://uiplaybook.dev/">Playbook</a>&nbsp;into a&nbsp;<a href="https://nextjs.org/">Next.js</a>&nbsp;project. &#128540;</p><h2>When should I use ViewComponent?</h2><p>I still use helper and partials; ViewComponent is just a new tool. Here are my rules about how to use it</p><p>I <strong>use</strong> a view component when</p><ul><li><p>I am considering extracting a partial that will be used in 2+ controllers</p></li><li><p>considering extracting a view helper that generates HTML</p></li><li><p>have complicated deep nested if-elsif-else (domain in view)</p></li><li><p>copy a lot of logic around</p></li><li><p>have to connect with JavaScript</p></li></ul><p>I <strong>don't</strong> use ViewComponent when:</p><ul><li><p>a partial is only used in one controller (example: _form.html.erb)</p></li><li><p>view helper, which is a simple pure function (example: format_money)</p></li><li><p>there is a lot of HTML on one single page - leave it there &#129335;&#8205;&#9794;&#65039;</p></li></ul><h3>Tips</h3><p><em>Can't name a post "Tips for" if there aren't some actionable tips in the post</em>&nbsp;&#129488;</p><ol><li><p>Have a &#8220;component&#8221; helper</p></li><li><p>Have &#8220;ApplicationComponent</p></li><li><p>Aliasing slots</p></li><li><p>Editor enhancements</p></li></ol><p>Lets break those down:</p><p><strong>1/ Have a "component" helper.</strong></p><pre><code>&lt;%= render FieldsetComponent(title: 'title') %&gt;

// becomes 

&lt;%= component :field_set, title: 'title' %&gt;</code></pre><p>It is a small change, but it makes components feel more "at home" in Rails' view. Here is a gist of its implementation &#128073;&nbsp;<a href="https://gist.github.com/RStankov/1117fcde73c70e5d4695119d77398ce0#file-application_helper-rb">link</a></p><p><strong>2/ Have "ApplicationComponent" base class similar to "ApplicationRecord", "ApplicationController".</strong></p><p><a href="https://gist.github.com/RStankov/1117fcde73c70e5d4695119d77398ce0#file-application_component-rb">Mine</a>&nbsp;has only two methods, but I still find it useful.</p><p><strong>3/ Alias "slots" to make code more readable</strong></p><p>ViewComponent has the concept of&nbsp;<a href="https://viewcomponent.org/guide/slots.html">slots</a>. It allows you to inject content inside your component. Very useful. However, the slots methods are prefixed with "with_," which I found less readable, so I often hide&nbsp;the fact&nbsp;I'm using a slot.</p><pre><code>class StatsComponent &lt; ApplicationComponent
  renders_many :numbers, StatsNumberComponent

  alias number with_number
end</code></pre><pre><code>// I find this less readable
&lt;%= component :stats do |c| %&gt;
  &lt;% c.with_number :balance %&gt;
&lt;% end %&gt;

// than the alias version
&lt;%= component :stats do |c| %&gt;
  &lt;% c.number :balance %&gt;
&lt;% end %&gt;</code></pre><p><strong>4/ Have switch to alternative shortcut</strong></p><p>I use<a href="https://github.com/tpope/vim-projectionist?tab=readme-ov-file#alternate-files"> vim-projectionist</a> and it allows me to define shortcuts to switch between component class and template. Very handy. &#128526;</p><h2>The builder pattern</h2><p>One&nbsp;of the more "advanced" patterns I started doing with ViewComponent is the "builder pattern". It is similar to how Rails FormBuilder looks like. You are calling methods of the ViewComponent to define how to display the component.</p><p>Here is an example of this pattern for "FilterFormComponent"</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lqCm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2629ba3a-1137-4ca6-9eeb-90856682d5a1_2380x128.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lqCm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2629ba3a-1137-4ca6-9eeb-90856682d5a1_2380x128.png 424w, https://substackcdn.com/image/fetch/$s_!lqCm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2629ba3a-1137-4ca6-9eeb-90856682d5a1_2380x128.png 848w, https://substackcdn.com/image/fetch/$s_!lqCm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2629ba3a-1137-4ca6-9eeb-90856682d5a1_2380x128.png 1272w, https://substackcdn.com/image/fetch/$s_!lqCm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2629ba3a-1137-4ca6-9eeb-90856682d5a1_2380x128.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lqCm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2629ba3a-1137-4ca6-9eeb-90856682d5a1_2380x128.png" width="1456" height="78" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2629ba3a-1137-4ca6-9eeb-90856682d5a1_2380x128.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:78,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:51101,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lqCm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2629ba3a-1137-4ca6-9eeb-90856682d5a1_2380x128.png 424w, https://substackcdn.com/image/fetch/$s_!lqCm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2629ba3a-1137-4ca6-9eeb-90856682d5a1_2380x128.png 848w, https://substackcdn.com/image/fetch/$s_!lqCm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2629ba3a-1137-4ca6-9eeb-90856682d5a1_2380x128.png 1272w, https://substackcdn.com/image/fetch/$s_!lqCm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2629ba3a-1137-4ca6-9eeb-90856682d5a1_2380x128.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><pre><code>&lt;%= component :filter_form, params: params do |form| %&gt;
  &lt;% form.search :query %&gt;
  &lt;% form.select :user_id, options: @search.user_options %&gt;
  &lt;% form.select :source, options: @search.source_options %&gt;
  &lt;% form.date_range :date %&gt;
  &lt;% form.select :kind, options: @search.kind_options %&gt;
&lt;% end %&gt;</code></pre><p><a href="https://speakerdeck.com/rstankov/component-driven-ui-with-viewcomponent-gem">In my talk</a>, I go through even more powerful example of the builder pattern in my TableComponent, where you use the builder pattern to define rules for each cell:</p><pre><code>&lt;%= component :table, @search.results do |table| %&gt;
  &lt;% table.record :name, :itself  %&gt;
  &lt;% table.record :apartment %&gt;
  &lt;% table.number :document do |record| %&gt;
    &lt;% record.documents.each do |document| %&gt;
      &lt;%= link_to document.display_number, document_path(document) %&gt;
    &lt;% end %&gt;
  &lt;% end %&gt;
  &lt;% table.record :cashier, :user %&gt;
  &lt;% table.date :date %&gt;
  &lt;% table.money :cash_reserve_amount %&gt;
  &lt;% table.money :wallet_amount %&gt;
  &lt;% table.money :total_amount %&gt;
  &lt;% table.column :kind do |record| %&gt;
    &lt;%= component :transaction_kind_badge, record %&gt;
  &lt;% end %&gt;
  &lt;% table.actions do |record| %&gt;
    &lt;%= button_details transaction_path(record) %&gt;
  &lt;% end %&gt;
&lt;% end %&gt;</code></pre><p>You can check the code for this &#128073; <a href="https://gist.github.com/RStankov/1117fcde73c70e5d4695119d77398ce0">here</a>.</p><h3>Why not use Phlex?</h3><p><a href="https://www.phlex.fun/">Phlex</a>&nbsp;is another way to add UI components in Rails applications. It also allows you to put your components in Ruby classes.</p><p>The main difference from ViewComponent is that it uses a Ruby&nbsp;<a href="https://en.wikipedia.org/wiki/Domain-specific_language">DSL</a>&nbsp;to define the HTML for the component.&nbsp;</p><p>It is awesome, and if&nbsp;<a href="https://viewcomponent.org/">ViewComponent</a>&nbsp;hadn't existed, I would have used it.</p><p>However, I prefer to use&nbsp;<a href="https://viewcomponent.org/">ViewComponent</a>&nbsp;because</p><ol><li><p>I prefer ERB over the DSL. I don't use components for everything; I only use them for reusable code. Thus, I often move code between ERB and DSL, and converting it from and to the DSL will slow me down. Or if I copied the code from ChatGPT, it will slow me down. Plus, DSL is something else I need to learn.</p></li><li><p><a href="https://viewcomponent.org/">ViewComponent</a>&nbsp;is backed and used by both&nbsp;<a href="https://github.com/">Github</a>&nbsp;and&nbsp;<a href="https://about.gitlab.com/">Gitlab</a>, which gives me confidence in the project's longevity.</p></li></ol><blockquote><p><em>Those same reasons might be why someone else will choose Phlex over ViewComponent&#8212;context matters. &#128517;</em></p></blockquote><h2>Conclusion</h2><p>For even more tips and tricks about ViewComponent, my talk is&nbsp;<a href="https://speakerdeck.com/rstankov/component-driven-ui-with-viewcomponent-gem">here</a>&nbsp;with a&nbsp;<a href="https://www.youtube.com/watch?v=ar8RMbDPoSY">video</a>. &#128584;</p><p>Choosing ViewComponent was one of the best architecture decisions I made for&nbsp;<a href="https://viewcomponent.org/">ViewComponent</a>. I'll definitely use it again if I'm starting a new Rails project (where Rails renders views).&nbsp;</p><p>ViewComponent should be built into Rails, too&nbsp;bad DHH&nbsp;<a href="https://discuss.rubyonrails.org/t/do-basecamp-hey-use-viewcomponents/83270/9">is not a fan</a>. &#129335;&#8205;&#9794;&#65039;</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading. Subscribe for free to receive more tips and support my work. &#128588;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>If you have any questions or comments, you can ping me on&nbsp;<a href="https://www.threads.net/@rstankov">Threads</a>,&nbsp;<a href="https://www.linkedin.com/in/radoslavstankov/">LinkedIn</a>,&nbsp;<a href="https://mastodon.social/@rstankov">Mastodon</a>, <a href="https://twitter.com/rstankov">Twitter</a> or just leave a comment below &#128237;</p>]]></content:encoded></item><item><title><![CDATA[Tips for Opening Pull-Requests]]></title><description><![CDATA[Good pull requests invite comments. Here are some tips and tricks for improving your pull requests and making them easier to review.]]></description><link>https://tips.rstankov.com/p/tips-for-opening-pull-requests</link><guid isPermaLink="false">https://tips.rstankov.com/p/tips-for-opening-pull-requests</guid><dc:creator><![CDATA[Radoslav Stankov]]></dc:creator><pubDate>Fri, 19 Apr 2024 16:10:08 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/2edf1150-d473-4631-a77f-8efbceb96e8a_1024x1024.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There are many tips on how to define the code review process and how to perform reviews. I want to share tips on opening a pull request so it is easier for you and your code reviewer.</p><blockquote><p>Comments on your pull requests are <strong>privilege</strong>.<br>Receiving many comments does not imply that you have done a poor job.<br>It means your code is understandable enough and inspired someone to give you suggestions.</p></blockquote><p>It's common for developers to feel discouraged when they receive comments on their code review.&nbsp;</p><p>However, good pull requests invite comments. Most people won't bother to properly review code that they don't understand and just mark it as "<em>looks fine</em>".</p><p>Here are my tips for opening good pull requests:</p><ol><li><p><a href="https://tips.rstankov.com/i/143746817/open-small-feature-sliced-pull-requests">Open small feature sliced pull-requests</a></p></li><li><p><a href="https://tips.rstankov.com/i/143746817/record-a-video-walkthrough-of-the-feature">Record a video walkthrough of the feature</a></p></li><li><p><a href="https://tips.rstankov.com/i/143746817/code-review-your-code">Code review your code</a></p></li><li><p><a href="https://tips.rstankov.com/i/143746817/have-a-structured-name-and-description">Have a structured name and description</a></p></li><li><p><a href="https://tips.rstankov.com/i/143746817/have-review-buddy">Have a review buddy</a></p></li></ol><p>Let&#8217;s break these down.</p><h3>1/ Open small feature sliced pull-requests</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://twitter.com/iamdevloper/status/397664295875805184" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!iSlW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623e87a1-9d5f-4a39-a187-c4e9797c45e3_1144x620.png 424w, https://substackcdn.com/image/fetch/$s_!iSlW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623e87a1-9d5f-4a39-a187-c4e9797c45e3_1144x620.png 848w, https://substackcdn.com/image/fetch/$s_!iSlW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623e87a1-9d5f-4a39-a187-c4e9797c45e3_1144x620.png 1272w, https://substackcdn.com/image/fetch/$s_!iSlW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623e87a1-9d5f-4a39-a187-c4e9797c45e3_1144x620.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!iSlW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623e87a1-9d5f-4a39-a187-c4e9797c45e3_1144x620.png" width="718" height="389.12587412587413" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/623e87a1-9d5f-4a39-a187-c4e9797c45e3_1144x620.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:620,&quot;width&quot;:1144,&quot;resizeWidth&quot;:718,&quot;bytes&quot;:81160,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:&quot;https://twitter.com/iamdevloper/status/397664295875805184&quot;,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!iSlW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623e87a1-9d5f-4a39-a187-c4e9797c45e3_1144x620.png 424w, https://substackcdn.com/image/fetch/$s_!iSlW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623e87a1-9d5f-4a39-a187-c4e9797c45e3_1144x620.png 848w, https://substackcdn.com/image/fetch/$s_!iSlW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623e87a1-9d5f-4a39-a187-c4e9797c45e3_1144x620.png 1272w, https://substackcdn.com/image/fetch/$s_!iSlW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623e87a1-9d5f-4a39-a187-c4e9797c45e3_1144x620.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Since Github doesn't support <a href="https://newsletter.pragmaticengineer.com/p/stacked-diffs">Stacked Diffs</a>, the PRs I open go directly to the main branch.</p><p>I'm a heavy user of <a href="https://martinfowler.com/articles/feature-toggles.html">Feature Toggles</a>. They allow me to split work on the feature and deploy it before it is ready for public consumption.</p><p>I open at least 1 pull request daily when working on a feature.</p><p>Avoid opening pull requests by layer - one pull request for database migrations, then for the backend, then for the front end, and finally for tests. Often, when you get to the front end, you might notice that your database structure needs to be adjusted.&nbsp;</p><p>I prefer to split pull requests into feature slices containing a part of a feature implemented end to end.</p><p>For large features, I use the "<a href="https://blog.rstankov.com/how-i-plan-and-execute-features/">diagram as todo" technique</a>, where I have a diagram representing the flow of the feature and color-code which parts of the feature are read and which are being worked on.&nbsp;</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!iDu8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc72a04b0-1377-47b7-9f18-92619d539272_3002x1052.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!iDu8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc72a04b0-1377-47b7-9f18-92619d539272_3002x1052.png 424w, https://substackcdn.com/image/fetch/$s_!iDu8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc72a04b0-1377-47b7-9f18-92619d539272_3002x1052.png 848w, https://substackcdn.com/image/fetch/$s_!iDu8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc72a04b0-1377-47b7-9f18-92619d539272_3002x1052.png 1272w, https://substackcdn.com/image/fetch/$s_!iDu8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc72a04b0-1377-47b7-9f18-92619d539272_3002x1052.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!iDu8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc72a04b0-1377-47b7-9f18-92619d539272_3002x1052.png" width="1456" height="510" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c72a04b0-1377-47b7-9f18-92619d539272_3002x1052.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:510,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:239318,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!iDu8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc72a04b0-1377-47b7-9f18-92619d539272_3002x1052.png 424w, https://substackcdn.com/image/fetch/$s_!iDu8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc72a04b0-1377-47b7-9f18-92619d539272_3002x1052.png 848w, https://substackcdn.com/image/fetch/$s_!iDu8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc72a04b0-1377-47b7-9f18-92619d539272_3002x1052.png 1272w, https://substackcdn.com/image/fetch/$s_!iDu8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc72a04b0-1377-47b7-9f18-92619d539272_3002x1052.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ul><li><p><strong>Green</strong> - feature slice that is done</p></li><li><p><strong>Yellow</strong> - feature slice in this pull request</p></li><li><p><strong>White</strong> - feature slice to be done</p></li><li><p><strong>Gray</strong> / <strong>Red</strong> - feature slice that won't be implemented</p></li></ul><p>I include a screenshot of the diagram in the pull request description</p><p>You can learn more about this "<a href="https://blog.rstankov.com/how-i-plan-and-execute-features/">diagram as todo" technique</a>" &#128073;&nbsp;<a href="https://blog.rstankov.com/how-i-plan-and-execute-features/">here</a>.</p><h3>2/ Record a video walkthrough of the feature</h3><p>&#128161; <strong>I think this is the most important tip from this post</strong>.</p><p>Before opening a pull request for review, I record a video walkthrough of this feature. There, I explain the feature and go through it.&nbsp;</p><p>I currently use&nbsp;<a href="https://www.loom.com/)">Loom</a>&nbsp;for this purpose.</p><p>I have done it so many times that now, I need 1 take, and in 3~4 minutes, I can explain even the most complex features.</p><p><strong>I can't count how often I start recording and notice a small bug or UI/UX issue.&nbsp;</strong>&#129321;</p><p>I will share a link to this video in the pull request's description</p><p>When the feature is deployed, I share it in Slack with the rest of the team.</p><p>In&nbsp;<a href="https://angrybuilding.com/">Angry Building</a>, we have a `#changelog` channel where I share the Loom videos with the whole company. </p><p>Afterward, our support team shares these videos with customers who would benefit from the new features.</p><p>Our marketing team will use those videos to compile our changelog email for customers.</p><h3>3/ Code review your code</h3><p>After I'm done with Loom, I perform a self-code review. This has also helped me to catch a lot of issues before sending my code to be reviewed by others &#128373;&#65039;&#8205;&#9794;&#65039;</p><p>I go through the PR, pretending I didn't write it. I review it the same way I'll review someone else's pull request.</p><p>I look at every file and mark it as "Viewed" (<em>later, if I need to make changes, this will help me to see what I have changed</em>).</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!atPh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2594c46-2889-49a7-9481-b4202aef0408_1125x122.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!atPh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2594c46-2889-49a7-9481-b4202aef0408_1125x122.png 424w, https://substackcdn.com/image/fetch/$s_!atPh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2594c46-2889-49a7-9481-b4202aef0408_1125x122.png 848w, https://substackcdn.com/image/fetch/$s_!atPh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2594c46-2889-49a7-9481-b4202aef0408_1125x122.png 1272w, https://substackcdn.com/image/fetch/$s_!atPh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2594c46-2889-49a7-9481-b4202aef0408_1125x122.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!atPh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2594c46-2889-49a7-9481-b4202aef0408_1125x122.png" width="1125" height="122" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c2594c46-2889-49a7-9481-b4202aef0408_1125x122.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:122,&quot;width&quot;:1125,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:16994,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!atPh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2594c46-2889-49a7-9481-b4202aef0408_1125x122.png 424w, https://substackcdn.com/image/fetch/$s_!atPh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2594c46-2889-49a7-9481-b4202aef0408_1125x122.png 848w, https://substackcdn.com/image/fetch/$s_!atPh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2594c46-2889-49a7-9481-b4202aef0408_1125x122.png 1272w, https://substackcdn.com/image/fetch/$s_!atPh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2594c46-2889-49a7-9481-b4202aef0408_1125x122.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>I even add comments if I need to provide extra context.</p><p>I added questions in places where I'm not sure about the approach.</p><h3>4/ Have a structured name and description</h3><p>I prefix related pull requests with the name of the domain or feature. So things are grouped.</p><p>Here are a couple of examples:</p><pre><code>[Teams] Invite modal
[Teams] Invite email flow
[Topics] Assign to product
[Topics] Index page
[Funds] Transfer via transaction
[Funds] Change transaction fund
[Funds] Move between funds</code></pre><p>For the pull request description, I used the following template in my projects:</p><pre><code>[Ticket link](https://link-to-external-system)

[Description]

[Loom video]

## Screenshots

## Code improvements</code></pre><ul><li><p>Ticket link to Trello, Asana, Jira, or whatever project management system is used</p></li><li><p>Description includes</p><ul><li><p>What is this pull request for</p></li><li><p>Implementation details</p></li><li><p>Embedded screenshot of a&nbsp;&nbsp;<a href="https://blog.rstankov.com/how-i-plan-and-execute-features/">diagram as todo</a></p></li></ul></li><li><p>Loom video from tip 2</p></li><li><p>Screenshots, when user-facing feature</p><ul><li><p>desktop and mobile versions</p></li></ul></li><li><p>Code improvements</p><ul><li><p>List of system-level changes included as part of this pull-request</p><ul><li><p>Example: &#8220;<em>Extracted new SelectList generic React component</em>&#8221;</p></li></ul></li><li><p>Sometimes, those can be split into separate pull request</p></li></ul></li></ul><h3>5/ Have review buddy</h3><p>When working with small pull requests, reviewers can often lose the thread and miss the bigger picture.&nbsp;</p><p>Because of this, I work closely with someone on my team to be my "buddy" reviewer for a bigger feature. I always wait for this person to merge my PRs and communicate with them about my features.&nbsp;</p><p>Even if this person is not directly involved in my project, I leverage their perspective as&nbsp;a&nbsp;sounding board from day 1. Their insights and input help refine my work and ensure it's on the right track</p><h2>Conclusion</h2><p>Your job is to make your pull request good enough to receive comments. Good pull requests invite comments. A lot of the tips here sound like "too much work". However, with practice, those become second nature.&nbsp;</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading. Subscribe for free to receive more tips and support my work. &#128588;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>If you have any questions or comments, you can ping me on&nbsp;<a href="https://www.threads.net/@rstankov">Threads</a>,&nbsp;<a href="https://www.linkedin.com/in/radoslavstankov/">LinkedIn</a>,&nbsp;<a href="https://mastodon.social/@rstankov">Mastodon</a>, <a href="https://twitter.com/rstankov">Twitter</a> or just leave a comment below &#128237;</p>]]></content:encoded></item><item><title><![CDATA[Tips for Dealing With External Services]]></title><description><![CDATA[... we all don't like them but have to deal with them &#128517;]]></description><link>https://tips.rstankov.com/p/tips-for-dealing-with-external-services</link><guid isPermaLink="false">https://tips.rstankov.com/p/tips-for-dealing-with-external-services</guid><dc:creator><![CDATA[Radoslav Stankov]]></dc:creator><pubDate>Wed, 10 Apr 2024 18:06:30 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/5e55b1c1-4243-4f09-8cb1-18c1b675e13e_1452x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I don't like external services, as most developers do, but businesses like them. For good reasons (on both sides). </p><p>The inherent issue is that they are out of our control.</p><p>Here are some of the common issues with external services </p><ol><li><p>Lack of documentation - every system is different</p></li><li><p>Dealing with API keys</p></li><li><p>Unexpected errors</p></li><li><p>Performance issues </p></li><li><p>API throttling - too many calls and they kill requests</p></li><li><p>API pricing - usually you pay for the service, but also recently there is a new trend of paying for API access</p></li><li><p>How to integrate with the application</p></li><li><p>Lack of safe sandbox playground - often, you have to call the life system to test while developing.</p></li><li><p>Unexpected changes of those APIs</p></li></ol><p>In my career, I have integrated with many external services. Here are some of my tips how to deal with them.</p><ol><li><p><a href="https://tips.rstankov.com/i/143455915/keep-external-services-code-in-a-single-place">Keep external services code in a single place</a> &#128205; </p></li><li><p><a href="https://tips.rstankov.com/i/143455915/wrap-external-services-code-in-a-facade">Wrap external services code in a facade</a> &#127917;</p></li><li><p><a href="https://tips.rstankov.com/i/143455915/store-credentials-to-external-services-with-links">Store credentials to external services with links</a> &#128274;</p></li><li><p><a href="http://Keep a log of calls to external services in a database">Keep a log of calls to external services in a database</a> &#128202;</p></li><li><p><a href="http://Perform calls to external services in background jobs">Perform calls to external service&#1089; in background jobs</a> &#9881;&#65039;</p></li></ol><p>Let's break these down.</p><h2>1/ Keep external services code in a single place &#128205; </h2><p><strong>All</strong>&nbsp;external services live in a single place. </p><p>If I use Ruby on Rails, I have an `<code>external</code>` module, living in `<code>app/lib/external/[service].rb</code>`. </p><p>If I'm using React, they all live in `<code>utils/external/[sercice].ts</code>`.</p><p>This even includes code for microservices I control.</p><p>By adopting this practice, I can efficiently monitor most of the outgoing network calls from my application by simply looking at this folder.</p><h2>2/ Wrap external services code in a facade &#127917;</h2><p>I ALWAYS wrap all external services with a <a href="https://en.wikipedia.org/wiki/Facade_pattern">facade</a>. </p><p>Even when they have their libraries.</p><p>In this way</p><ul><li><p>I know exactly what is being used from the service</p><ul><li><p>Updating service code or even replacing service is easier</p></li></ul></li><li><p>It is easier to test our code because I can stub the service facade.</p></li><li><p>There is a single place, to put documentation </p></li><li><p>Makes upgrading the service API very easy since you know where its impact points are</p></li></ul><p><strong>Ruby example</strong></p><p>I use `extend self` <a href="https://en.wikipedia.org/wiki/Facade_pattern">facade</a> module in Ruby, named `<code>External::[Service]Api</code>`. Usually, those are just bags of methods.</p><p>Here is a template for external services:</p><pre><code># frozen_string_literal: true

# Documentation
#
# - service: [url to service]
# - manage: [url to manage tokens]
# - api: [url to api documentation]
# - gem: [gem we are using (optional)]
# [- ... other links]

module External::[Service]Api
  extend self

  # documentation: [documentation]
  def perform_action(args)
    # use HTTParty or gem for this external service
  end
end</code></pre><p>Here is an example for using <a href="https://unsplash.com/">Unsplash</a> api:</p><pre><code># frozen_string_literal: true

# Documentation
#
# - service: https://unsplash.com/
# - api: https://unsplash.com/documentation
# - gem: https://github.com/unsplash/unsplash_rb
# - portal: https://unsplash.com/oauth/applications

module External::UnsplashApi
  extend self

  # documentation: https://unsplash.com/documentation#search-photos
  def search(query)
    Unsplash::Photo.search(query, 1, 12, 'landscape')
  end

  # documentation: https://unsplash.com/documentation#get-a-photo
  # documentation: https://unsplash.com/documentation#track-a-photo-download
  def track_download(id)
    photo = Unsplash::Photo.find(id)
    photo.track_download
  rescue Unsplash::NotFoundError
    nil
  end
end</code></pre><p>Notice all the links to the documentation here. </p><p>Even when the external service has a gem to access it, the gem is never used outside this module. </p><p>If the gem returns an instance of a response object, I try to wrap it with a class I own. I expose only what I use from the services.</p><p><strong>JavaScript example</strong></p><p>I follow the same rules in rules in JavaScript. There, instead of a module, I export functions. I still have links to the documentation in the comments.</p><p>Here is an example of an <a href="https://www.intercom.com/">Intercom</a> facade:</p><pre><code>let identified = false;

export function identifyUser(userInfo) {
  if (identified) {
    return;
  }

  if (userInfo.userId) {
    runIntercom('onShow', () =&gt; {
      window.Intercom('update', userInfo);

      identified = true;
    });
  }
}

export function openMessage(message) {
  runIntercom('showNewMessage', message);
}

export showProductTour(tourId) {
  runIntercom('startTour', tourId);
}

function runIntercom(cmd: string, args: any) {
  if (typeof window === 'undefined' || !window.Intercom) {
    return;
  }

  return window.Intercom(cmd, args);
}</code></pre><p>External JavaScript libraries often depend on injecting scripts. I try to include those in `<code>external</code>` as well.</p><pre><code>For example, exporting the Intercom script tag lives as `<code>utils/external/intercom/script</code>`. 
// Documentation: https://app.intercom.com/a/apps/[code]/settings/web
export default `
  (function() {
    window.intercomSettings = {
      app_id: "[code]"
    };
    (function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',w.intercomSettings);}else{var d=document;var i=function(){i.c(arguments);};i.q=[];i.c=function(args){i.q.push(args);};w.Intercom=i;var l=function(){setTimeout(function(){var s=d.createElement('script');s.type='text/javascript';s.defer=true;s.src='https://widget.intercom.io/widget/[code]';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}, 4000);};if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})();
  })()
`;</code></pre><h2>3/ Store credentials to external services with links&#128274;</h2><p>The key concepts here are</p><ol><li><p>Don't put credentials in plain text. </p><ol><li><p>Except when you expose API keys in frontend, those will be available in frontend anyway</p></li></ol></li><li><p>Have credentials in a centralized placed </p></li><li><p>Have links where those credentials come from </p></li><li><p>Have comments who have added those credentials</p></li></ol><p>In Ruby on Rails, I use <a href="https://blog.engineyard.com/rails-encrypted-credentials-on-rails-5.2">encrypted credentials</a>.</p><p>Credentials are stored in <a href="https://en.wikipedia.org/wiki/YAML">YAML</a>. I always include a link to where a certain credential was taken because it is very irritating to search where a token and secret were taken, especially when dealing with Google APIs. </p><pre><code><code># Taken from: [url to where those credentials are taken]
[sevice_name]_token: [...]
[sevice_name]_secret: [...] </code></code></pre><p>I like to group all those credentials in an `<code>extend self module</code>` named `<code>Config</code>`, along with other app configurations.</p><p>Other technologies use primary <a href="https://en.wikipedia.org/wiki/Environment_variable">ENV</a> (Environment) variables for credentials. In this case. What I do is </p><ol><li><p>Access ENV variables only from a single config file</p><ol><li><p><em>...or two if I have server and client-specific ENV variables</em> </p></li></ol></li><li><p>Have links to where credentials were taken in this config file</p></li></ol><p><strong>I can't stress enough how important it is to have a link and explanation of where a certain API key comes from.</strong></p><p>It is very frustrating to wonder if this is the correct key. Has it expired? Which app uses it?</p><h2>4/ Keep a log of calls to external services in a database &#128202;</h2><p>This one depends a lot on the service call. If I make external calls to something like <a href="https://stripe.com/">Stripe</a>, I only keep the `<code>_external_id</code>` returned by <a href="https://stripe.com/">Stripe</a>.</p><p>In Angry Building, I leverage <a href="https://www.twilio.com/">Twilio</a> to send SMS to users. I maintain a database table named `<code>external_twillio_messages</code>` to keep track of every message and its deliverability. This helps me monitor the effectiveness of my SMS campaigns and aids in cost management, as  <a href="https://www.twilio.com/">Twilio</a> charges are based on the number of SMS messages sent. </p><p>I called <a href="https://developer.twitter.com/en">Twitter</a> API to get tweets in the past, and I only needed immutable data from them. <a href="https://developer.twitter.com/en">Twitter</a> had hard limits in terms of API rating. Having a database table `external_twitter_tweets` to store tweets helps me only to fetch data once. </p><h2>5/ Perform calls to external services in background jobs &#9881;&#65039;</h2><p>Instead of making service calls directly in your application during web requests, you move to a <a href="https://en.wikipedia.org/wiki/Background_process">background job</a>. Those jobs should be <a href="https://en.wikipedia.org/wiki/Idempotence">idempotent</a>.</p><p>When you call external services, you're making network calls. By moving these calls out of your application's main execution path and into background jobs, you are optimizing your application's performance.</p><p>In this way, if those services are down or there are network issues, your background job system will handle them.</p><p>You can combine with my previous advice and have a database table for external calls and have a flow like the following:</p><ol><li><p>Create a record in the database with the pending status </p></li><li><p>Trigger background job</p></li><li><p>When the background job is done, mark the record as complete </p></li><li><p>Notify the user when completed</p><ol><li><p>Refresh UI</p></li><li><p>Send notification</p></li></ol></li></ol><h2>Conclusion</h2><p>When you are dealing with external services, you should be very careful and isolate them from the rest of your system.</p><p>In this way, maintaining those external services is a lot easier.</p><p>Apart from that, I suggest leaving many comments with links to documentation about this external API. </p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading. Subscribe for free to receive more tips and support my work. &#128588;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>If you have any questions or comments, you can ping me on&nbsp;<a href="https://www.threads.net/@rstankov">Threads</a>,&nbsp;<a href="https://www.linkedin.com/in/radoslavstankov/">LinkedIn</a>,&nbsp;<a href="https://mastodon.social/@rstankov">Mastodon</a>, <a href="https://twitter.com/rstankov">Twitter</a> or just leave a comment below &#128237;</p>]]></content:encoded></item><item><title><![CDATA[Tips for Counter Caches]]></title><description><![CDATA[When working on database powered application, often performance issues come from: N+1 queries. Where we have a list of records, and we make extra queries for each record. Performing expensive count queries. Often, those queries need to do the whole table reads.]]></description><link>https://tips.rstankov.com/p/counter-cache</link><guid isPermaLink="false">https://tips.rstankov.com/p/counter-cache</guid><dc:creator><![CDATA[Radoslav Stankov]]></dc:creator><pubDate>Fri, 05 Apr 2024 07:59:29 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/7ce765f7-0d9c-4187-a510-690a24c161c5_1510x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When working on database powered application, often performance issues come from:</p><ol><li><p>N+1 queries. Where we have a list of records, and we make extra queries for each record.</p></li><li><p>Performing expensive count queries. Often, those queries need to do the whole table reads.</p></li></ol><p>A good illustrative example is this list of posts on a site like <a href="https://www.producthunt.com/">Product Hunt.</a> </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!esbU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce635f4-99e2-4e60-8d8f-a6a677207d37_1594x651.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!esbU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce635f4-99e2-4e60-8d8f-a6a677207d37_1594x651.png 424w, https://substackcdn.com/image/fetch/$s_!esbU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce635f4-99e2-4e60-8d8f-a6a677207d37_1594x651.png 848w, https://substackcdn.com/image/fetch/$s_!esbU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce635f4-99e2-4e60-8d8f-a6a677207d37_1594x651.png 1272w, https://substackcdn.com/image/fetch/$s_!esbU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce635f4-99e2-4e60-8d8f-a6a677207d37_1594x651.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!esbU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce635f4-99e2-4e60-8d8f-a6a677207d37_1594x651.png" width="1456" height="595" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2ce635f4-99e2-4e60-8d8f-a6a677207d37_1594x651.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:595,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:208901,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!esbU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce635f4-99e2-4e60-8d8f-a6a677207d37_1594x651.png 424w, https://substackcdn.com/image/fetch/$s_!esbU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce635f4-99e2-4e60-8d8f-a6a677207d37_1594x651.png 848w, https://substackcdn.com/image/fetch/$s_!esbU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce635f4-99e2-4e60-8d8f-a6a677207d37_1594x651.png 1272w, https://substackcdn.com/image/fetch/$s_!esbU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce635f4-99e2-4e60-8d8f-a6a677207d37_1594x651.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>How many queries do we need to make to implement this? &#129300;</p><blockquote><p>1 query for a list of posts (by some conditions)<br>then for every post following queries are needed<br>  1. comments count - "SELECT COUNT(*) FROM comments WHERE post_id = $1"<br>  2. likes count "SELECT COUNT(*) FROM likes WHERE post_id = $1"<br>  3. list of topics - "SELECT ..."<br>  4. has the current user liked "SELECT ...</p></blockquote><p>This makes it 4 queries for every post. If we have 100 posts, this is 401 queries. &#129763; &#128556;</p><p>In this post, I'm going to focus on two count queries - "comments count" and "likes count".&nbsp;<em>How to handle "list of topics" and "has current user liked" is something I'm going to cover in the future.</em></p><h2>Counter Cache</h2><p>The way to solve these queries is simple:</p><blockquote><p>Don't do those queries, store the "count" value in the "post" table. </p><p>This technique is called "<strong>counter cache</strong>". </p></blockquote><p>It is quite popular in the Ruby on Rails world. It is even built in to <a href="https://guides.rubyonrails.org/association_basics.html#methods-added-by-belongs-to">Active Record</a>.</p><p>The way the counter cache works is as follows</p><ul><li><p>"[parent]" table table have column named "[child]_count"</p></li><li><p>when rows from "[child]" tables</p><ul><li><p>are inserted, the "[parent].[child]_count" column is incremented</p></li><li><p>are deleted, the  "[parent].[child]_count" column is decremented</p></li></ul></li></ul><p>The following code in Ruby on Rails will automatically handles this:</p><pre><code>class Comment &lt; ActiveRecord::Base
  belongs_to :post, counter_cache: true
end</code></pre><p>Of course, we can implement this using pure SQL &#128073; <a href="https://gist.github.com/RStankov/342f637505845d1aa6179a66e1450ef1">This is how can be done in PostgreSQL</a>.</p><p>Surprisingly, most Node.js ORM libraries don't have this built-in, and even&nbsp;<a href="https://hexdocs.pm/ecto/Ecto.html">Elixir's Ecto</a>&nbsp;doesn't have it &#129327;</p><h2>Drawbacks</h2><p>Counter caches are great. So, I should have one for every association. &#129320;</p><p>Of course not. As with every technique, it has its tradeoffs.</p><p><strong>1) Wrong counters</strong></p><p>As with every type of cache, keeping it up-to-date and correct is important.  </p><p><strong>2)  Making write operations slow</strong></p><p>Counter cache causes additional workload on every write, update, or delete operation, thereby slowing down these operations. </p><p>For instance, when millions of likes are created on a single post at the same time, the post-likes counter will be updated excessively, which can result in database lock issues with post rows and cause slowdowns in vote insert operations. and slows those operations.</p><p>One solution is to use the <a href="https://planetscale.com/blog/the-slotted-counter-pattern">Slotted Counter Pattern</a>.</p><p>Another solution is moving counters from the database and in something like Redis.</p><p><strong>3) Increase storage costs</strong></p><p>Storage is cheap, yes, but being wasteful is expensive.</p><p>Counters are often needed for the most important tables in the application. Those are also the most inner-connected tables. So, if counter cache columns are hastily added, those already big tables get many extra columns.</p><p>Every extra column adds weight when there are tables with billions of records.</p><p><strong>4) Tooling for them outside of Active Record needs to be improved.</strong></p><p>Outside of Ruby on Rails, I mostly work with Node.js, and the ORM database handling situation isn't pretty. </p><p></p><h3>Tips for using counter caches with Active Record</h3><p>This section is about <a href="https://guides.rubyonrails.org/active_record_basics.html">Active Record</a> because this is where I have the most experience and because it has the best tooling. &#128584;</p><p>There are a couple of different ways to have a counter cache; when I add a counter cache, I start from the following list one by one and pick the first that works. &#128526;</p><p>1/ <strong>The built-in Active Record</strong> counter cache option for the `belongs_to` association. This is the most simple and fastest option, but it has limited flexibility.</p><pre><code>class Post &lt; ApplicationRecord
  belongs :category, counter_cache: true 
  
  # we explicitly the column name
  belongs :category, counter_cache: :posts_count
end</code></pre><p>2/ <strong>The <a href="https://github.com/magnusvk/counter_culture">counter_culture</a> gem</strong> with its wide range of options. <br><br>It supports switching between columns, increment/decrement with different values and etc.</p><pre><code>class Post &lt; ApplicationRecord
  belongs :category

  # track only the published posts count in category
  counter_culture(
     :category, 
     column_name: -&gt; { :published_post_count if _1.published? }
   )

  # track the sum of all words in articles in the category
  counter_culture(
     :category, 
     column_name: :total_words_count, 
     delta_magnitude: -&gt; { _1.words_count }
  )
end</code></pre><p><strong>3/ Custom counter cache code.</strong> </p><p>We need custom code to handle the counters when logic is so specific. For those, I use callbacks because this logic should be encapsulated.</p><p>Example: Having category -&gt; post -&gt; author relationship, where we aim for each category to have a "unique_authors_count", which hows the unique number of authors who have posts in this category. Implemented something like this following</p><pre><code>class Post &lt; ActiveRecord::Base
  belongs_to :post
  belongs_to :author

  after_save :update_category_unique_authors_count
  after_destroy :update_category_unique_authors_count

  private
  
  def update_category_unique_authors_count
    category.update_columns(
      unique_authors_count: category.posts.count('DISTINCT(user_id)'),
      updated_at: Time.current
    )
  end
end</code></pre><p>4/ Use <a href="https://github.com/evilmartians/activerecord-slotted_counters">slotted_counters</a> where the database table with counter cache is under heavy write load. </p><p>The idea is to split the counter column in to multiple tables. To prevent deadlocks. I used this very rarely. </p><p><a href="https://planetscale.com/blog/the-slotted-counter-pattern">More about this technique</a>. </p><pre><code>class Post &lt; ActiveRecord::Base
  has_slotted_counter :votes
end</code></pre><h3>Showcase: Not only for counts </h3><p>In <a href="https://angrybuilding.com/">Angry Building</a>, I have a lot of tables with numbers like like the following.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!U3r0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e6ddc03-5850-4742-b129-0da05d3bcdf4_2496x616.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!U3r0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e6ddc03-5850-4742-b129-0da05d3bcdf4_2496x616.png 424w, https://substackcdn.com/image/fetch/$s_!U3r0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e6ddc03-5850-4742-b129-0da05d3bcdf4_2496x616.png 848w, https://substackcdn.com/image/fetch/$s_!U3r0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e6ddc03-5850-4742-b129-0da05d3bcdf4_2496x616.png 1272w, https://substackcdn.com/image/fetch/$s_!U3r0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e6ddc03-5850-4742-b129-0da05d3bcdf4_2496x616.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!U3r0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e6ddc03-5850-4742-b129-0da05d3bcdf4_2496x616.png" width="1456" height="359" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5e6ddc03-5850-4742-b129-0da05d3bcdf4_2496x616.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:359,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:162189,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!U3r0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e6ddc03-5850-4742-b129-0da05d3bcdf4_2496x616.png 424w, https://substackcdn.com/image/fetch/$s_!U3r0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e6ddc03-5850-4742-b129-0da05d3bcdf4_2496x616.png 848w, https://substackcdn.com/image/fetch/$s_!U3r0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e6ddc03-5850-4742-b129-0da05d3bcdf4_2496x616.png 1272w, https://substackcdn.com/image/fetch/$s_!U3r0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e6ddc03-5850-4742-b129-0da05d3bcdf4_2496x616.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Imagine doing an extra query for every number in this screenshot. This will be too much. </p><p>Our building tables have the following columns </p><ul><li><p><strong>apartments_count</strong> - number of apartments</p></li><li><p><strong>balance_amount</strong> - counter of how much money is in the building</p></li><li><p><strong>monthly_fee_amount</strong> - how much fees in total pay all apartments</p></li><li><p><strong>obligations_amount</strong> - are how much fees are not paid</p></li><li><p><strong>debtors_count</strong> - how many apartments have obligations</p></li><li><p><strong>open_issues_count</strong> - number opened issues in the issue tracker, not total</p></li></ul><p>For all those counts, I use the <a href="https://github.com/magnusvk/counter_culture">counter_culture</a> gem</p><pre><code>class Issue &lt; ActiveRecord::Base
  belongs_to :building

  # only update "open_issues_count" when issue is opened
  counter_culture(
    :building, 
    column_name: -&gt; { :open_issues_count if _1.opened?
   }
end

class MoneyTranfer &lt; ActiveRecord::Base
  belongs_to :building

  # counter cache is sum of amount of the money transfer
  counter_culture(
    :building, 
    column_name: :balance_amount, 
    delta_magnitude: -&gt; { _1.amount }
  )
end

class Apartment &lt; ActiveRecord::Base
  # use default rails counter for "apartments_count"
  belongs_to :building, counter_cache: true

  # if apartment has obligations update counter
  counter_culture(
    :building,
    column_name: -&gt; { :debtors_count if _1.debtor? }
  )

  # sum all monthly fees for all apartments
  counter_culture(
   :building, 
   column_name: :monthly_fee_amount, 
   delta_magnitude: -&gt; { _1.monthly_fee_amount }
  )
end </code></pre><p>It is useful to have counts at the record level because it allows me to display them in the app UI without having to access the database. For example, I can decide whether or not to show a new payment button based on an apartment's unpaid fees.</p><h2>Conclusion</h2><p>Every time I create new tables, I think about how the records from those tables will be used, whether I need counter caches, and so on. I reach for counter cache columns very often. </p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading. Subscribe for free to receive more tips and support my work. &#128588;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>If you have any questions or comments, you can ping me on&nbsp;<a href="https://www.threads.net/@rstankov">Threads</a>,&nbsp;<a href="https://www.linkedin.com/in/radoslavstankov/">LinkedIn</a>,&nbsp;<a href="https://mastodon.social/@rstankov">Mastodon</a>, <a href="https://twitter.com/rstankov">Twitter</a> or just leave a comment below &#128237;</p>]]></content:encoded></item><item><title><![CDATA[Tips for Dealing With Emergences]]></title><description><![CDATA[The process I used at Product Hunt and Angry Building to deal with emergencies. Using an &#9937;&#65039; EmergencyKit]]></description><link>https://tips.rstankov.com/p/tips-for-dealing-with-emergences</link><guid isPermaLink="false">https://tips.rstankov.com/p/tips-for-dealing-with-emergences</guid><dc:creator><![CDATA[Radoslav Stankov]]></dc:creator><pubDate>Tue, 12 Mar 2024 13:16:17 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/245b6a77-998a-437b-837b-ba79963db22a_1181x848.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I was at Product Hunt, emergencies were very rare.</p><p>Between January 2019 (when I started counting) and March 2023 (when I left <a href="https://www.producthunt.com/">Product Hunt</a> to co-found <a href="https://angrybuilding.com/">Angry Building</a>), there were only 25 recorded emergencies. This means there was less than one emergency per month. Some of those emergencies were not user-facing.&nbsp;</p><p>We also had a very stable team. Our team was fully distributed between America, Europe, and Asia, so we had 24-hour coverage, and there was always an experienced engineer online.&nbsp;</p><p>Because of this, in my 8 years there, we didn't have an on-call process.</p><p>However, in early 2021, after we had grown the team, we experienced a few nasty outages in a row. During these, the core team members were unavailable, leaving relatively new and unprepared team members to deal with the incidents.</p><p>Due to this, I decided to tighten our ad-hoc process.&nbsp;</p><p>Nevertheless, I did not introduce an on-call role. Instead, I made dealing with incoming emergencies easier for everyone on the team.&nbsp;</p><p>The process of dealing with an emergency has 3 components. I have a similar process at Angry Building;</p><ol><li><p><a href="https://tips.rstankov.com/i/142397368/detect-emergencies">Detect emergencies</a></p></li><li><p><a href="https://tips.rstankov.com/i/142397368/deal-with-emergencies">Deal with emergencies</a></p></li><li><p><a href="https://tips.rstankov.com/i/142397368/improve-system-after-emergency">Improve the system after emergency</a></p></li></ol><p>In this post, I'm going to walk through those components.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!sb47!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ae4ad75-ab50-4fa0-a7f9-18fcdf04f94e_1784x500.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sb47!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ae4ad75-ab50-4fa0-a7f9-18fcdf04f94e_1784x500.png 424w, https://substackcdn.com/image/fetch/$s_!sb47!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ae4ad75-ab50-4fa0-a7f9-18fcdf04f94e_1784x500.png 848w, https://substackcdn.com/image/fetch/$s_!sb47!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ae4ad75-ab50-4fa0-a7f9-18fcdf04f94e_1784x500.png 1272w, https://substackcdn.com/image/fetch/$s_!sb47!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ae4ad75-ab50-4fa0-a7f9-18fcdf04f94e_1784x500.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!sb47!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ae4ad75-ab50-4fa0-a7f9-18fcdf04f94e_1784x500.png" width="1456" height="408" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2ae4ad75-ab50-4fa0-a7f9-18fcdf04f94e_1784x500.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:408,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:128842,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!sb47!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ae4ad75-ab50-4fa0-a7f9-18fcdf04f94e_1784x500.png 424w, https://substackcdn.com/image/fetch/$s_!sb47!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ae4ad75-ab50-4fa0-a7f9-18fcdf04f94e_1784x500.png 848w, https://substackcdn.com/image/fetch/$s_!sb47!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ae4ad75-ab50-4fa0-a7f9-18fcdf04f94e_1784x500.png 1272w, https://substackcdn.com/image/fetch/$s_!sb47!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ae4ad75-ab50-4fa0-a7f9-18fcdf04f94e_1784x500.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>1) Detect emergencies</h2><p>It is important to have a central place where all detection goes. I like to use Slack for this.</p><ul><li><p>&#8220;<strong>feedback</strong>&#8221;&#8212;this is where all the rest of the company posts feedback exclusively for emergencies. This is when someone notices an issue. You want this to be THE LAST place you find out about emergencies.</p></li><li><p>&#8220;<strong>auto-engineering-sentry</strong>&#8221; - I like to use Sentry for exception tracking. This channel tracks every new exception. Usually, those are the first signs something is breaking. Both Product Hunt and Angry Building have very low numbers of unhandled exceptions.</p></li><li><p>&#8220;<strong>auto-engineering-alerts</strong>&#8221; - a catch-up channel receiving messages from services like PagerDuty (or <a href="http://betterstack.com">betterstack.com</a>), AWS CloudWatch, etc. Monitoring tools often have a way to alert you in Slack. I always have this channel on notification.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!fJtD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7282641-d4e3-4612-bd21-be1c4019285e_1472x154.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fJtD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7282641-d4e3-4612-bd21-be1c4019285e_1472x154.png 424w, https://substackcdn.com/image/fetch/$s_!fJtD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7282641-d4e3-4612-bd21-be1c4019285e_1472x154.png 848w, https://substackcdn.com/image/fetch/$s_!fJtD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7282641-d4e3-4612-bd21-be1c4019285e_1472x154.png 1272w, https://substackcdn.com/image/fetch/$s_!fJtD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7282641-d4e3-4612-bd21-be1c4019285e_1472x154.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!fJtD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7282641-d4e3-4612-bd21-be1c4019285e_1472x154.png" width="1456" height="152" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f7282641-d4e3-4612-bd21-be1c4019285e_1472x154.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:152,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:55998,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!fJtD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7282641-d4e3-4612-bd21-be1c4019285e_1472x154.png 424w, https://substackcdn.com/image/fetch/$s_!fJtD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7282641-d4e3-4612-bd21-be1c4019285e_1472x154.png 848w, https://substackcdn.com/image/fetch/$s_!fJtD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7282641-d4e3-4612-bd21-be1c4019285e_1472x154.png 1272w, https://substackcdn.com/image/fetch/$s_!fJtD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7282641-d4e3-4612-bd21-be1c4019285e_1472x154.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p><em>(example of alert from "auto-engineering-alerts")</em></p><blockquote><p>&#128161; Tip about Slack channel naming </p><p>I like to follow the `prefix` naming scheme with Slack channels - "[type]-[team]-[name]".<br>I use the `auto` type for channels that receive messages from external systems.</p></blockquote><p>The key is to ensure all channels maintain low noise ratios, especially the "auto-engineering-alerts" ones. It should be an event when something is posted there. </p><p>If you haven't found out about an outage from an automated tool, one of the first action items in your Postmortems should be - "update your monitoring to detect outages"  (<em>more on Postmortems later in this post</em>)</p><h2>2) Deal with emergencies</h2><p>Here, we can use two tools "the engineering-emergency" Slack channel and the &#9937;&#65039; "Emergency Kit"&nbsp; documents</p><p></p><p>The Slack channel is where everyone gathers during an emergency; all communication is done there.&nbsp;</p><p>The benefits of this are:</p><ul><li><p>Coordinating people&nbsp; - you don't want one person reverting a deploy while another attempts to deploy a quick-fix.</p></li><li><p>Makes writing postmortem about the incident easier because the whole history is there</p></li><li><p>There is a history of previous emergencies that can used as inspiration</p></li><li><p>The whole company sees that action is being taken and doesn't overload engineers' DMs.</p><ul><li><p>For bigger incidents, the support team is in the loop so that they can communicate with our users about the incident</p></li></ul></li></ul><p>The Slack channel also has a link to the "Emergency Kit."&nbsp;</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LL2y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d14972e-d53c-4229-9d8b-6ccceb5ad5f8_512x175.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LL2y!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d14972e-d53c-4229-9d8b-6ccceb5ad5f8_512x175.png 424w, https://substackcdn.com/image/fetch/$s_!LL2y!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d14972e-d53c-4229-9d8b-6ccceb5ad5f8_512x175.png 848w, https://substackcdn.com/image/fetch/$s_!LL2y!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d14972e-d53c-4229-9d8b-6ccceb5ad5f8_512x175.png 1272w, https://substackcdn.com/image/fetch/$s_!LL2y!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d14972e-d53c-4229-9d8b-6ccceb5ad5f8_512x175.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LL2y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d14972e-d53c-4229-9d8b-6ccceb5ad5f8_512x175.png" width="512" height="175" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0d14972e-d53c-4229-9d8b-6ccceb5ad5f8_512x175.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:175,&quot;width&quot;:512,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:17878,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!LL2y!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d14972e-d53c-4229-9d8b-6ccceb5ad5f8_512x175.png 424w, https://substackcdn.com/image/fetch/$s_!LL2y!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d14972e-d53c-4229-9d8b-6ccceb5ad5f8_512x175.png 848w, https://substackcdn.com/image/fetch/$s_!LL2y!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d14972e-d53c-4229-9d8b-6ccceb5ad5f8_512x175.png 1272w, https://substackcdn.com/image/fetch/$s_!LL2y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d14972e-d53c-4229-9d8b-6ccceb5ad5f8_512x175.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><blockquote><p>&#128161; Tip about Slack channels</p><p>One of my favorite unutilized Slack features is that you can add links to channels. It is especially useful for "project-" channels where you can link to project management tools, documents, metrics, etc. </p></blockquote><h3>&#9937;&#65039; The Emergency Kit</h3><p>It is a tutorial document and links a bag of information on what to do during an emergency.</p><p>It has the following parts.</p><ol><li><p>A <strong>video walkthrough</strong> of how to diagnose issues - should be quick (2- 3 minutes) and straight to the point. This is very useful for first-time responders.</p></li><li><p><strong>Tools</strong> - links to various dashboards and tools used to manage the application.</p></li><li><p><strong>Process</strong> - quickly illustrated with diagrams (see example).</p></li><li><p><strong>Tips &amp; Tricks</strong></p></li><li><p><strong>Common Issues and Solutions.</strong></p></li><li><p><strong>How-tos</strong> - <em>after tools, this is the area I use the most.</em></p></li></ol><p><a href="https://gist.github.com/RStankov/37a8098d0f7053d2210bd570f94f2fc0">Here a gist</a> with my template for the Emergency Kit &#128073; <a href="https://gist.github.com/RStankov/37a8098d0f7053d2210bd570f94f2fc0">&#9937;&#65039; Emergency Kit</a></p><p>You will notice the example document uses a lot of links and <code>&lt;details&gt;</code> HTML tags. The document should be a starting point and hub that is usable for novice and experienced responders.&nbsp;</p><p>I even knew all the systems by heart; I'll still use the document for the links and to copy/paste commands from how-tos.</p><p>The best way to handle issues is to start your exception tracker and then go to the database, application servers, and CDN.</p><p>If the issue isn't immediately obvious, I always follow those steps.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!MfOS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c5d06d1-ac40-4a36-aea3-929ef712b78c_2000x851.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MfOS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c5d06d1-ac40-4a36-aea3-929ef712b78c_2000x851.png 424w, https://substackcdn.com/image/fetch/$s_!MfOS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c5d06d1-ac40-4a36-aea3-929ef712b78c_2000x851.png 848w, https://substackcdn.com/image/fetch/$s_!MfOS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c5d06d1-ac40-4a36-aea3-929ef712b78c_2000x851.png 1272w, https://substackcdn.com/image/fetch/$s_!MfOS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c5d06d1-ac40-4a36-aea3-929ef712b78c_2000x851.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MfOS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c5d06d1-ac40-4a36-aea3-929ef712b78c_2000x851.png" width="1456" height="620" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1c5d06d1-ac40-4a36-aea3-929ef712b78c_2000x851.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:620,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:175364,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!MfOS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c5d06d1-ac40-4a36-aea3-929ef712b78c_2000x851.png 424w, https://substackcdn.com/image/fetch/$s_!MfOS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c5d06d1-ac40-4a36-aea3-929ef712b78c_2000x851.png 848w, https://substackcdn.com/image/fetch/$s_!MfOS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c5d06d1-ac40-4a36-aea3-929ef712b78c_2000x851.png 1272w, https://substackcdn.com/image/fetch/$s_!MfOS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c5d06d1-ac40-4a36-aea3-929ef712b78c_2000x851.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The usual issues I have encountered throughout my career are&nbsp;</p><ul><li><p>Changes are not appearing after deploy</p><ul><li><p>This happens if you have <a href="https://docs.aws.amazon.com/whitepapers/latest/overview-deployment-options/rolling-deployments.html">rolling deployments</a> and the new version of the application can't boot. Often, exception tracking doesn't catch this, and you need to monitor one level below. If you use AWS, you can have Cloudwatch alerts for this.</p></li></ul></li><li><p>Site is very slow (<em>more than usual</em>)</p><ul><li><p>The corporate here often is the database. Usually, this is a symptom of something happening - traffic spike, expensive query, etc.</p></li><li><p>Another option might be to use more servers/containers to handle the load.</p></li><li><p>Check the status pages of the cloud provider (if you use a cloud provider). Once, we had an issue due to an AWS networking outage in our region.</p></li></ul></li><li><p>There are a lot of 500s</p><ul><li><p>Check your exception tracker; it will tell you what is failing</p></li><li><p>If there isn't anything there, the corporate is between services</p></li></ul></li></ul><p>Some evergreen tips for handling issues include:</p><ul><li><p>Revert deploys till the last working deploy</p><ul><li><p>Beware if there were database changes</p></li><li><p>When services depend on each other, like frontend JS app and backend API API, revert together</p></li></ul></li><li><p>Check all recent code changes</p><ul><li><p>focus first on the database, updated dependencies or infrastructure changes</p></li><li><p>look a couple days to a week back,</p><ul><li><p>once, we had a nasty memory leak deployed a week ago</p></li><li><p>it was slowly creeping into our RAM</p></li><li><p>We saw it when we looked month data back on our RAM initialization</p></li></ul></li></ul></li><li><p>When browsing, monitoring data</p><ul><li><p>Expand to 48 hour period and look for spikes in charts</p></li><li><p>Watch for response time, CPU, Memory</p></li></ul></li><li><p>Isolate the issue to the lowest point in the tech stack</p></li><li><p>Have a way to do hotfixes deploys and bypass bureaucracy like every change should have a code review</p><ul><li><p>You should have a way even to deploy without full CI tests suit pass, to build a docker image and push.</p></li><li><p>This should only be used in emergencies and should be restricted otherwise. You can have a restriction in GitHub to push to the main directly, but in an emergency, the org admin should be able to allow those deploys temporarily.</p></li></ul></li></ul><p>Things that should be included in the "how-to" area and known by everyone is</p><ul><li><p>How to restart services</p></li><li><p>How to revert deploy</p></li><li><p>How to change the number of servers/containers/tasks/pods, etc</p></li><li><p>How to quickly deploy hotfix</p></li></ul><p>Throughout my career, these are the common issues I have observed.</p><h3>3) Improve system after emergency</h3><p>After the emergency is handled, one of the engineers is assigned to write a blameless postmortem.</p><p>Here is a <a href="https://gist.github.com/RStankov/82aeb2604043b81df5b35f3c6a4a327b">gist</a> of the postmortem template I use &#128073; <a href="https://gist.github.com/RStankov/82aeb2604043b81df5b35f3c6a4a327b">link</a>.</p><p>It is very useful to keep a database of all postmortems in a tool like Notion. </p><p>Two interesting tips we do with postmortems is </p><p>1. There are explicit action points to improve the emergency kit </p><p>2. We do a system walkthrough with the whole engineering team to review the steps taken so everyone is prepared for the next emergency.</p><p><em>We started having a video of the walkthrough, but no one was watching it, so it was better to do it on call. </em>&#129488;</p><p>I used the postmortem to improve documentation, monitoring, and system residency.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading. Subscribe for free to receive more tips and support my work. &#128588;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>If you have any questions or comments, you can ping me on&nbsp;<a href="https://www.threads.net/@rstankov">Threads</a>,&nbsp;<a href="https://www.linkedin.com/in/radoslavstankov/">LinkedIn</a>,&nbsp;<a href="https://mastodon.social/@rstankov">Mastodon</a>, <a href="https://twitter.com/rstankov">Twitter</a> or just leave a comment below &#128237;</p>]]></content:encoded></item><item><title><![CDATA[Tips for Building a Modal System in React]]></title><description><![CDATA[Implementing a robust modal system is key for most apps, with similar needs despite varying requirements. Here's a guide on how to do it effectively]]></description><link>https://tips.rstankov.com/p/tips-for-building-modal-system-in</link><guid isPermaLink="false">https://tips.rstankov.com/p/tips-for-building-modal-system-in</guid><dc:creator><![CDATA[Radoslav Stankov]]></dc:creator><pubDate>Wed, 21 Feb 2024 22:51:43 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/1d564b55-3770-4422-962f-22ad77ae9da2_1024x1024.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most applications require a modal (or dialog) system. While their requirements might differ, they are quite similar for the most part. A challenging aspect is that some requirements are often implicit, with the modal system being part of another feature.</p><p>Consider a feature like this:</p><blockquote><p>As a shop owner,<br>I want the ability to add a new product category via a modal directly from the product creation form,<br>so that I don't have to leave the form to create a new category if one doesn't exist.</p></blockquote><p>Developers often scramble to build a modal, or nowadays, they might ask ChatGPT (and go for a coffee while <a href="https://chat.openai.com/share/d977f4fa-7056-44ca-9c99-76b1383df257">ChatGPT generates the code</a>).</p><p>Most tutorials suggest something like the following:</p><pre><code>const [isOpen, setOpen] = useState(false);

const openModal = () =&gt; {
  setOpen(true)
}

&lt;&gt;
  &lt;button 
    onClick={openModal}&gt;
    Open
  &lt;/button&gt;
  {isOpen &amp;&amp; (
    &lt;Portal&gt;
      &lt;Modal /&gt;
    &lt;/Portal&gt;
  )}
&lt;/&gt;</code></pre><p><em>&#8230;and this is <a href="https://chat.openai.com/share/d977f4fa-7056-44ca-9c99-76b1383df257">what ChatGPT generated</a>.</em> </p><p>This approach, while seemingly straightforward, is naive. It works for managing a single modal but lacks basic features, such as closing the modal with the "ESC" key. </p><p>To say this approach is naive is an understatement. It <em>kinda</em> works if you have only one modal. It doesn't have basic features like  "ESC" to close the modal. </p><p>What is worse it introduces a lot of boilerplate code, leading to a development pattern filled with copy-paste solutions. &#128580;</p><p>So, let's see how to architect a better modal system.</p><h2>Requirements</h2><p>Before starting to build, it's crucial to consider what needs to be built. </p><p>Here are some of the obvious requirements for a good modal system:</p><ul><li><p>Provide an easy way to open and close modals programmatically.</p></li><li><p>Allow modals to return a "result," for example, selecting a product from a search.</p></li><li><p>Modal can be closed by</p><ul><li><p>close button </p></li><li><p>ESC button</p></li><li><p>click/tap outside the modal</p></li></ul></li><li><p>Ensure <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-modal">accessibility</a></p></li><li><p>Support different layout or frame types, such as a white box, full-screen gallery, etc</p></li></ul><p>Less obvious requirements include:</p><ul><li><p>Modals can have URLs</p><ul><li><p>Example: In <a href="https://www.producthunt.com/">Product Hunt</a>, posts can be opened in modal form or as separate pages</p></li><li><p>We should define how the back button is handled</p><ul><li><p>If the modal has a URL, the back should close the modal</p></li></ul></li></ul></li><li><p>Modals can be stacked. A modal can open another modal.</p><ul><li><p>Example: Modal is for product creation and can trigger modal for category creation.</p></li></ul></li><li><p>Scroll position</p><ul><li><p>Don't scroll the background page</p></li><li><p>Preserve the scroll position on the page</p></li><li><p>Preserve scroll position on previously open modals when stacking</p></li></ul></li><li><p>Data fetching</p><ul><li><p>Lazy loading of the JS/CSS code for modal components</p></li><li><p>API fetching for the content of the models</p></li></ul></li><li><p>Handle modal layout on mobile and desktop.</p></li><li><p>There should be a way for components rendered in inside or outside modals to know this</p><ul><li><p>Example: Infinite scroll component should listen for scroll events of a document or top modal</p></li></ul></li></ul><h2>Architecting the Modal System</h2><p>I call "<em>support system</em>", a code whose main purpose is to help developers build features. </p><p>The modal system is a good example of such a system. Most of the time, developers should build specific modals or open modals. They interact with the system and don't change the system often.</p><p>I tend to approach the "support systems" as functional calls. They should be "black boxes" or functions. Developers should care about their input and output, not much about their implementation. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qezs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F183b36ed-86d3-4d9f-b854-d0bd01e93d10_1872x502.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qezs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F183b36ed-86d3-4d9f-b854-d0bd01e93d10_1872x502.png 424w, https://substackcdn.com/image/fetch/$s_!qezs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F183b36ed-86d3-4d9f-b854-d0bd01e93d10_1872x502.png 848w, https://substackcdn.com/image/fetch/$s_!qezs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F183b36ed-86d3-4d9f-b854-d0bd01e93d10_1872x502.png 1272w, https://substackcdn.com/image/fetch/$s_!qezs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F183b36ed-86d3-4d9f-b854-d0bd01e93d10_1872x502.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qezs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F183b36ed-86d3-4d9f-b854-d0bd01e93d10_1872x502.png" width="1456" height="390" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/183b36ed-86d3-4d9f-b854-d0bd01e93d10_1872x502.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:390,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:72188,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!qezs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F183b36ed-86d3-4d9f-b854-d0bd01e93d10_1872x502.png 424w, https://substackcdn.com/image/fetch/$s_!qezs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F183b36ed-86d3-4d9f-b854-d0bd01e93d10_1872x502.png 848w, https://substackcdn.com/image/fetch/$s_!qezs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F183b36ed-86d3-4d9f-b854-d0bd01e93d10_1872x502.png 1272w, https://substackcdn.com/image/fetch/$s_!qezs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F183b36ed-86d3-4d9f-b854-d0bd01e93d10_1872x502.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>When designing a "<em>support system</em>", the most important thing is to</p><ol><li><p><strong>Make the common operations easy</strong>  (<em>the public interface)</em></p></li><li><p>Put most of the complexity in the "<em>black box</em>"  (<em>the implementation details</em>)</p></li></ol><p>You should be able to change implementation details without changing the interface. In this way, you can have a partial implementation of the system and still use it to implement features.</p><p>For the modal system, the most common operations are:</p><ul><li><p>Open modals</p></li><li><p>Implementing modal components</p></li></ul><p>Let's start with those interfaces.</p><h2>Open Modal</h2><p>The API for opening a modal, which I have used for years has an `openModal` function, which opens a modal component.</p><pre><code>openModal({
  content: &lt;MyModal /&gt;
});</code></pre><p>This function should be able to be called from anywhere. It only depends on something.</p><p>`openModal` accepts an object so that we can pass different options, like:</p><pre><code>openModal({
  path: '/posts/id',
  frame: 'box',
  onClose: closeHandle,
  content: (
    &lt;Post postId={id} /&gt;
  ),
});</code></pre><p>With this simple interface, we are future proved:</p><ul><li><p>We can easily add more options in the future, like, for example, "sound", which triggers different sounds when the modal is open/closed &#128266;</p></li><li><p>We don't care if stacking is implemented. <em>Maybe in v1, we don't add stacking for modals. &#128168;</em></p></li><li><p>How we render the modal can also be changed later</p></li></ul><p>One detail that might be missed is the `content` attribute. There, we pass a rendered React component. We can do this because JSX passed converts to a JS structure that we can pass around.</p><p>We can pass props to this component. This is how we handle the "<em>return result</em>" from the modal. We use a callback. &#128584;</p><pre><code>openModal({
  content: (
   &lt;CreateCategoryModal 
     onCreate={onCreate} /&gt;
  ),
});</code></pre><p>I have tried many more ways to pass data as a modal result, but the good old callback is still the best. &#128170;</p><h2>Modal Component</h2><p>This is where most of the implementation work will be taken. When "modal system" is mentioned, this is where people usually think - how modals look.</p><pre><code>function MyModal() {
  return (
    &lt;ModalUI.Frame&gt;
      &lt;ModalUI.Title /&gt;
      &lt;ModalUI.Body /&gt;
      &lt;ModalUI.Footer /&gt;
    &lt;/ModalUI.Frame&gt;
  )
}</code></pre><p>This is useful, and I also use this depending on the UI in the project. However, we can do even better. </p><p>Often, models need to fetch data to operate. This should be built into the model setup.</p><p>For pages, I usually extract a helper named <a href="https://blog.rstankov.com/structuring-next-js-application/#technique4createpage">createPage</a> to help me with things like data fetching.</p><p>I do the same for modals - have a helper named `createModal` that deals with data fetching and modal.</p><p>Here is how it can look:</p><pre><code>export default createModal({
  // Let's say we use GraphQL
  // Code will be similar to REST API
  query: GRAPHQL_QUERY,
  queryVariables: (props) =&gt; { ... },

  // This sets document.title
  title: (data) =&gt; data.post.title,

  // We can have other useful props
  frame: 'box',

  // Component UI
  renderComponent: ({ data, ...props }) {
    return ...
  },
});</code></pre><p>But why not use a simple `useQuery` hook? Why all of this? &#129300;</p><p>"<strong>Make the common operations easy</strong>". If we have a  `useQuery`, we still need to manually write code every time to show the loading indicator, deal with errors, handle changing the document titles.</p><p>The next thing I like to do with a modal is to lazy load their code with things like &nbsp;<a href="https://nextjs.org/docs/advanced-features/dynamic-import">next/dynamic</a>, so they are not included in the page bundle because modal content is often optional for the page.<br></p><p>Here is how that layout is out.</p><pre><code>/components/MyModal/index.js
import dynamic from 'next/dynamic';

import dynamic from 'next/dynamic';
import Loader from '@/utils/modals/Loader';

export default dynamic(() =&gt; import('./Content'), {
    loading: () =&gt; &lt;Loader /&gt;,
});


/components/MyModal/Content.tsx
... modal itself</code></pre><p>For the modal, they <code>import MyModal from @/components/MyModal</code>, and all the rest is handled automatically.</p><ol><li><p>Next.js lazy load the modal. We show a nice animated loader for this</p></li><li><p>`createModal` loads the content. It shows the same animated loader, so there is no content shift.</p></li></ol><p>The `createModal` is self-contained. Some models might not use it at all. <a href="https://gist.github.com/RStankov/8c05fef0104aa55305f272d60407d3f7#file-create-tsx">You can check a sample implementation of it in this gist</a>. &#128187;</p><h2>The System Implementation</h2><p>We have the public interface of our system. Now, let's go into technical details.  </p><p>The `openModal` and `closeModal` use the <a href="https://tips.rstankov.com/p/eventbus-in-react-applications">EvenBus from my previous post</a>.</p><pre><code>export function openModal(options) {
  emit('modalOpen', options);
}

export function closeModal() {
  emit('modalClose');
}</code></pre><p>The event bus does the heavy lifting for this. I first extracted <a href="https://tips.rstankov.com/p/eventbus-in-react-applications">EventBus</a> for a modal system.</p><p>The event bus sends those events in a `ModalContainer` component. The responsibilities of these components are:</p><ul><li><p>Listens for open/closed events </p></li><li><p>Renders models and their overlays</p></li><li><p>Handles stacking of modals</p></li><li><p>Deals with scrolling</p></li><li><p>Handles close of modals via esc, overlay</p></li></ul><p>This is quite a lot and full of technical details.</p><p>The whole modal system without the UI elements is about ~ 400 lines of TypeScript. </p><p>Putting in a post will be too long (<em>even for me</em>). &#128517;</p><p>Because of this, I have extracted the code into a <strong><a href="https://gist.github.com/RStankov/8c05fef0104aa55305f272d60407d3f7">gist</a></strong>  with detailed comments.</p><p>&#128073;  <a href="https://gist.github.com/RStankov/8c05fef0104aa55305f272d60407d3f7">Code of a working modal system</a> &#128187;</p><p>Here is a video walkthrough of the code. &#127871;</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;3f11f025-1c08-4919-b0c7-17fd91e6b28c&quot;,&quot;duration&quot;:null}"></div><h2>Q&amp;A</h2><p><strong>Why not make this an open-source package?</strong></p><p>I always needed more time to do so. <em>I might do it someday.</em></p><p>One tricky area is that the modal system often needs various tweaks for each project. </p><p>Generalizing it as an open-source package might get too complex and heavy.</p><p><strong>What do you think about the new HTML dialog element? How does it fit with this structure?</strong></p><p>I like "dialog" html element. I use it a lot at Angry Building. </p><p>More people should use it.</p><p>I like this minor feature a lot -&gt; when the dialog is opened and there are form elements, the first element auto-focuses.</p><p>What I have shown here will work with or without `dialog` </p><h2>Conclusion</h2><p>Now you have the wireframe of a good modal system. Of course, every application has its requirements.</p><p>One "<em>meta</em>" thing about the design of this system is the clear separation between the public interface and implementation details.&nbsp;</p><p>We&nbsp;<strong>made the common operations easy&nbsp;</strong>and hid enough implementation details so we could easily extend the system over time.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading. Subscribe for free to receive more tips and support my work. &#128588;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>If you have any questions or comments, you can ping me on&nbsp;<a href="https://www.threads.net/@rstankov">Threads</a>,&nbsp;<a href="https://www.linkedin.com/in/radoslavstankov/">LinkedIn</a>,&nbsp;<a href="https://mastodon.social/@rstankov">Mastodon</a>, <a href="https://twitter.com/rstankov">Twitter</a> or just leave a comment below &#128237;</p>]]></content:encoded></item><item><title><![CDATA[EventBus in React Applications]]></title><description><![CDATA[Almost every system I've worked on needed elements like toasts, modals, and popovers. Over the last few years, I've utilized a straightforward patten to standardize the implementation of these components.]]></description><link>https://tips.rstankov.com/p/eventbus-in-react-applications</link><guid isPermaLink="false">https://tips.rstankov.com/p/eventbus-in-react-applications</guid><dc:creator><![CDATA[Radoslav Stankov]]></dc:creator><pubDate>Wed, 07 Feb 2024 17:39:10 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d16f7a28-539f-423f-a520-8ce07a08aff1_1024x1024.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Almost every system I've worked on needed UI elements like <a href="https://open-ui.org/components/toast.research/">toasts</a>, modals, and popovers. Over the last few years, I've utilized a straightforward patten to standardize the implementation of these components.</p><h2>Let's start with the system for &#8220;toasts&#8221;</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WI6T!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cb791c3-485d-46a9-902f-b79f6b2b3b82_818x262.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WI6T!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cb791c3-485d-46a9-902f-b79f6b2b3b82_818x262.png 424w, https://substackcdn.com/image/fetch/$s_!WI6T!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cb791c3-485d-46a9-902f-b79f6b2b3b82_818x262.png 848w, https://substackcdn.com/image/fetch/$s_!WI6T!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cb791c3-485d-46a9-902f-b79f6b2b3b82_818x262.png 1272w, https://substackcdn.com/image/fetch/$s_!WI6T!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cb791c3-485d-46a9-902f-b79f6b2b3b82_818x262.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WI6T!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cb791c3-485d-46a9-902f-b79f6b2b3b82_818x262.png" width="818" height="262" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6cb791c3-485d-46a9-902f-b79f6b2b3b82_818x262.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:262,&quot;width&quot;:818,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:26862,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!WI6T!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cb791c3-485d-46a9-902f-b79f6b2b3b82_818x262.png 424w, https://substackcdn.com/image/fetch/$s_!WI6T!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cb791c3-485d-46a9-902f-b79f6b2b3b82_818x262.png 848w, https://substackcdn.com/image/fetch/$s_!WI6T!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cb791c3-485d-46a9-902f-b79f6b2b3b82_818x262.png 1272w, https://substackcdn.com/image/fetch/$s_!WI6T!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cb791c3-485d-46a9-902f-b79f6b2b3b82_818x262.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Back in <a href="https://blog.rstankov.com/how-i-use-react-context/">September 2020</a>, I wrote about how I built a toast system using <a href="https://react.dev/reference/react/createContext">React.Context</a>.</p><p>I encapsulated the logic within a <code>ToastProvider</code>, and the toast component itself lived in <code>ToastProvider.Content</code>.</p><pre><code>import { ToastProvider } from '~/utils/toast'

// the "Provider" pyramid
&lt;ApolloProvider&gt;
  &lt;ToastProvider&gt;
    &lt;ModalProvider&gt;
      &lt;Layout&gt;
        {children}
      &lt;/Layout&gt;
      // Reads the state and displays the toast
      &lt;ToastProvider.Content /&gt;
      &lt;ModalProvider.Content /&gt;
    &lt;/ModalProvider&gt;
  &lt;/ToastProvider&gt;
&lt;/ApolloProvider&gt;</code></pre><p>I had a hook to open the toast.</p><pre><code>import { useToast } from '~/utils/toast'

export default function ShowToast() {
  const open = useToast();

  const onClick = () =&gt; open({
    title: 'This is the title for this prompt',
    content: &lt;strong&gt;Content&lt;/strong&gt;,
  });

  return &lt;button onClick={onClick}&gt;open&lt;/button&gt;;
}</code></pre><p>For more details <a href="https://blog.rstankov.com/how-i-use-react-context/">check that article</a>.</p><p>This worked well but had a few drawbacks:</p><ol><li><p>The &#8220;Provider&#8221; pyramid</p></li><li><p>When the provider value changes, the entire tree is re-rendered</p></li><li><p>Accessing functions to display a toast requires a hook. Therefore, it needs to be retrieved from a React component and passed around</p><ol><li><p>For instance, to display a toast from a Socket, React wrappers were necessary</p></li></ol></li><li><p>I needed two components &#8220;Provider&#8221; and &#8220;Content&#8221;, so in order to understand the system you needed to read them both</p></li></ol><p>I resolved these issues by using event bus pattern (<a href="https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern">publish/subscribe</a>). With it I, eliminated the need for <a href="https://react.dev/reference/react/createContext">React.Context</a>.</p><h2>Enter evenBus</h2><pre><code>import { emit, useEventBus } from '~/utils/eventBus';</code></pre><p>It is a simple module with 2 public methods</p><p><code>emit(eventName, payload)</code> triggers an event from anywhere in my system.</p><p><code>useEventBus(eventName, (payload) =&gt; handle(payload))</code> is a React hook that listens for the event and executes a function with the event payload upon receipt. The hook also cleans up event handlers when components are unmounted.</p><p>That is it &#128526;</p><p>These simple primitives are utilized whenever I need to build systems for modals, toasts, notices, popovers, and similar components. In react-native I used this to handle <code>signIn</code> / <code>signOut</code> or <code>languageChange</code> flows.</p><p>Let's see how the Toast implementation changes when we have <code>evenBus</code>.</p><pre><code>import { emit, useEventBus } from '~/utils/eventBus';

// I wrap `emit` with the functions exposed from the module
// Event bus is just an implementation detail now
// Those are more used that "Content", so they are first in the file
export function openToast(toast) {
  emit('toastClose', toast);
}

export function closeToast() {
  emit('toastClose');
}

// Only Toast Content is needed
export function ToastContent() {
  // Internal state of toast
  // For more complex systems, toast can be stacked
  // We can implemented stacking without changing the external interface
  const [toast, setToast] = useState&lt;IToast | null&gt;(null);
   
  // When toast is open, update state
  useEventBus('toastOpen', (toast) =&gt; {
    setToast(toast);
  });

  // When toast is closed, clear state
  useEventBus('toastClose', () =&gt; {
    setToast(null);
  });

  // Handle when there is no toast
  if (!toast) {
    return null;
  }

  return (
    &lt;!-- render toast --&gt;
  );
}
</code></pre><p>This <code>ToastContent</code> should be placed somewhere within the React tree. No restrictions.</p><p>Now, we can use <code>openToast</code> without a hook.</p><pre><code>import { openToast } from '~/utils/toast'

export default function ShowToast() {
  const onClick = () =&gt; openToast({
    title: 'This is the title for this prompt',
    content: &lt;strong&gt;Content&lt;/strong&gt;,
  });

  return &lt;button onClick={onClick}&gt;open&lt;/button&gt;;
}</code></pre><p>This approach resolves all the issues I encountered with the React.Context implementation.</p><ol><li><p>No need for Providers.</p></li><li><p>Reduced number of re-renders.</p></li><li><p>Fewer hooks required.</p></li><li><p>Ability to open toasts from anywhere.</p><ol><li><p>Integrating calls from a Socket, for example, becomes straightforward.</p></li></ol></li><li><p>Whole toast system is one simple file</p></li></ol><h2>Implementing modal system with evenBus</h2><p>Exactly the same.  &#128526;</p><p>A robust modal system needs to address:</p><ul><li><p>Modal stacking.</p></li><li><p>Modals with URLs.</p></li><li><p>Data loading for modals.</p></li><li><p>Returning results from modals.</p></li><li><p>Lazy loading of modal JavaScript code.</p></li><li><p>Scrolling.</p></li><li><p>Keyboard support.</p></li></ul><p>This is big enough for its own post, ... so I wrote one  &#128073; &#8220;<a href="https://tips.rstankov.com/p/tips-for-building-modal-system-in">building a modal system</a>&#8220;  &#128584;</p><h2>Implementation of eventBus itself</h2><p>Here is the implementation of the eventBus. I've been copying and pasting from project to project since around 2021. </p><pre><code>// Utilizing this small, dependency-free, ~100-line library
import mitt from 'mitt';

// Mapping events to their payloads
// TypeScript ensures correct event and payload usage
// Also serves as documentation for supported events
interface IEventsMap {
  // Event 'modalOpen' expects 'content' and optional 'url'
  // I name events with [module][action] format
  modalOpen: {
    content: React.ReactNode;
    url?: string;
  };

  // Passing `null` means no payload 
  modalClose: null;

  toastOpen: {
    title: string;
    content?: IToastContent;
    icon?: IToastIcon;
  }
  toastClose: null

  // Domain events can also be defined here
  commentCreated: {
     id: string;
     // ...
  };
  // ...
}

// Emitting a global event to the bus
export function emit&lt;T extends keyof IEventsMap&gt;(
  event: T,
  ...payload: IEventArguments&lt;T&gt;
) {
  if (typeof window !== 'undefined') {
    eventBus.emit(event, ...payload);
  } else {
    console.warn('emit called in server mode');
  }
}

// Hook for subscribing and unsubscribing to events
export function useEventBus&lt;T extends keyof IEventsMap&gt;(
  eventName: T,
  handler: (...payload: IEventArguments&lt;T&gt;) =&gt; void,
) {
  React.useEffect(() =&gt; {
    eventBus.on(eventName, handler);
    return () =&gt; eventBus.off(eventName, handler);
  }, [eventName, handler]);
}

// TypeScript helper type for matching an event with its payload type
// The most &#10024;  magical part of this file
type IEventArguments&lt;T extends keyof IEventsMap&gt; = 
  null extends IEventsMap[T] ? [] : [IEventsMap[T]];

// Creating one global instance of mitt
// This is an implementation detail
const eventBus = mitt() as {
  emit&lt;T extends keyof IEventsMap&gt;(
    e: T, ...p: IEventArguments&lt;T&gt;,
  ): void;
  on&lt;T extends keyof IEventsMap&gt;(
    e: T,
    f: (...p: IEventArguments&lt;T&gt;) =&gt; void,
  ): void;
  off&lt;T extends keyof IEventsMap&gt;(
    e: T,
    f: (...p: IEventArguments&lt;T&gt;) =&gt; void,
  ): void;
};
</code></pre><p>Here is a gist with the code above &#128073; <a href="https://gist.github.com/RStankov/93e49fb43b9043e7ff7be715185626eb">link</a>.</p><h2>Event Types</h2><p>In each project, the IEventsMap changes to accommodate the necessary events. I've considered moving this to a separate file when the list grows. However, so far, I have had 10-15 events at most.</p><p>In my projects, I have noticed, that there are two types of events:</p><ol><li><p>UI events - <code>toastOpen</code>,  <code>modalOpen</code>, <code>popoverOpen.</code></p></li><li><p>Domain events - <code>userSignIn</code>, <code>userSignOut</code>, <code>languageChanged</code>, <code>commentCreated</code>.</p></li></ol><p>Most of my events are UI-related because they can be triggered anywhere and have a clear purpose.</p><p>I seldom use domain events in projects. For domain-related activities, there are often better patterns than an event bus. </p><p>Typically, actions related to domain objects are contained within a single screen, where passing callbacks is clearer and more straightforward.</p><h2>Q&amp;A</h2><p><strong>Should every interaction be moved to the event bus?</strong></p><p>No, not. &#128581; </p><p>The events are useful in specific scenarios and not others. It excels when you need to trigger an action from anywhere in the system without expecting a response.</p><p>Domain events, in particular, should be used rarely. There are often better patterns that can be employed.</p><p><strong>Why not use redux / mobx / RxJS.... whatever</strong></p><p>Many of those solutions rely on React.Context and share the same limitations as my original toast provider. </p><p>State management varies from project to project. Most often, I don't use a state library, just Apollo. </p><p>The events in eventBus often don't change state; they report that the state was changed.</p><p><strong>Why not create an npm package?</strong></p><p>I've considered removing mitt and creating a react-mini-event-bus npm package. However, for now, the simplicity of maintaining ~50 lines of straightforward code works for me.</p><p>Plus, I need to figure out how to make IEventsMap configurable without requiring package users to redefine function signatures. If anyone has suggestions on accomplishing this, I'm all ears. &#128483;&#65039;&#128066;</p><p><strong>What about React Server Components (RSC)?</strong></p><p>Lately, everyone has asked about <a href="https://nextjs.org/docs/app/building-your-application/rendering/server-components">RSC</a> whenever React is mentioned. </p><p>I have yet to use them, as they don't address any issues I currently face. I'm not opposed to them.</p><h2>Conclusion</h2><p>The eventBus has significantly simplified several subsystems in my projects. However, as with every pattern, it should be used with care.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading. Subscribe for free to receive more tips and support my work. &#128588;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>If you have any questions or comments, you can ping me on&nbsp;<a href="https://www.threads.net/@rstankov">Threads</a>,&nbsp;<a href="https://www.linkedin.com/in/radoslavstankov/">LinkedIn</a>,&nbsp;<a href="https://mastodon.social/@rstankov">Mastodon</a>, <a href="https://twitter.com/rstankov">Twitter</a> or just leave a comment below &#128237;</p>]]></content:encoded></item><item><title><![CDATA[Tips for Testing]]></title><description><![CDATA[Testing is one of my favorite subjects. For me, automated testing has two goals: Enables faster iteration loops Ensure that when I change code, the old code behaves as it did before. In the projects I'm involved in, I try to make writing and running a test easier than executing the code in the browser manually.]]></description><link>https://tips.rstankov.com/p/my-testing-tips</link><guid isPermaLink="false">https://tips.rstankov.com/p/my-testing-tips</guid><dc:creator><![CDATA[Radoslav Stankov]]></dc:creator><pubDate>Thu, 25 Jan 2024 07:41:11 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/4cee0ed2-9f5a-4918-8650-3605619a27f7_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Testing is one of my favorite subjects. </p><p>For me, automated testing has two goals:</p><ol><li><p>Enables faster iteration loops</p></li><li><p>Ensure that when I change code, the old code behaves as it did before.</p></li></ol><p><strong>In the projects I'm involved in, I try to make writing and running a test easier than executing the code in the browser manually.</strong></p><p>I don't care if my tests are <a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself">DRY</a>; I even see it as an antipattern in tests.</p><p>I want my test to be:</p><ol><li><p>Reliable: they don't break randomly, don't fail when unrelated code is changed</p></li><li><p>Easy to understand: when I change logic, often the test needs to change as well</p></li><li><p>Fast: if tests are slow, people won't run them</p></li></ol><p>A good test suit doesn't replace good system monitoring, error tracking and alerting. </p><p>One of the barriers to entry for writing tests is the "ergonomics" of our tools. Requiring too much ceremony or boilerplate for every test will put off even the most enthusiastic testers.</p><p>One of the reasons testing is common in Ruby Land is tools do a lot of the setup/tear down for you. Much of this comes from the tight integration of libraries like&nbsp;<a href="https://rubyonrails.org/">Rails</a>,&nbsp;<a href="https://rspec.info/">RSpec</a>,&nbsp;<a href="https://github.com/thoughtbot/factory_bot">FactoryBot</a>, and&nbsp;<a href="https://github.com/teamcapybara/capybara">Capybara</a>.</p><p>In JavaScript land, things are decoupled, and you need to do too much setup/configuration. You must build your testing tools to integrate the different parts to have a sound testing story. Writing tests should be frictionless so that you can focus only on testing logic.</p><p>The second friction point in testing is speed - If it's faster to run the test, I'll do it more often. Having to wait makes you test less frequently. </p><p><em>The best tooling investment I ever made is integrating the <a href="https://github.com/vim-test/vim-test">vim-test</a> and running the current test from my editor with a simple shortcut.  </em>&#128519;</p><p>As we talk about friction, often, people don't know what to test or how to test something. This adds friction and makes people not to write tests.</p><p>When I design systems, removing this friction is very important for me.</p><p>Here are my tips and "<em>rules</em>" for writing tests, which have helped me spend less time deliberating over whether to write a test and encouraged me to just do it.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://tips.rstankov.com/subscribe?"><span>Subscribe now</span></a></p><p>Today, I&#8217;m going to cover </p><ul><li><p><a href="https://tips.rstankov.com/i/141004040/terms">Terms</a> </p></li><li><p><a href="https://tips.rstankov.com/i/141004040/the-shape-of-a-good-test">The Shape of a good test</a></p></li><li><p><a href="https://tips.rstankov.com/i/141004040/dealing-with-brittle-tests">How to deal with brittle tests</a></p></li><li><p><a href="https://tips.rstankov.com/i/141004040/ruby-on-rails-testing-tips">Ruby on Rails testing tips</a></p></li><li><p><a href="https://tips.rstankov.com/i/141004040/end-to-end-ee-testing-tips">End-to-End (E2E) testing tips</a></p></li><li><p><a href="https://tips.rstankov.com/i/141004040/javascript-testing-tips">JavaScript testing tips</a></p></li></ul><p>So, lets get started &#128073;</p><h2>Terms</h2><p>Let's start with some terms I'll be using. In the realm of automated testing (<em>as in everything in software engineering</em>), terms are all messed up.</p><p><strong><a href="https://en.wikipedia.org/wiki/System_under_test">SUD</a></strong>&nbsp;- a system under test; this is they mode of the system while it executes tests. Often is named &#8220;test environment&#8221;.</p><p><strong>E2E tests</strong>, known as end-to-end tests or feature specs in Ruby land. These are tests which verify a full feature from start to finish..</p><p>Example:&nbsp;<em>Login, fill in a form, and verify the form is stored in the database and email is sent</em>. These often involve running tests in actual or simulated browsers. </p><p>In the Ruby, we often use&nbsp;<a href="https://github.com/teamcapybara/capybara">Capybara</a>, while in the JavaScript world, tools like &nbsp;<a href="https://www.cypress.io/">Cypress</a> are common.</p><p><strong>Smoke tests</strong>&nbsp;are tests that verify code runs without an error but don't care much if the result of code execution is what is expected from the program. They just make sure nothing has blown up.</p><p>They are a good place to start when you don&#8217;t have tests or you do major rewrites.</p><p><strong>Brittle test</strong> - test that fails "<em>randomly</em>". The most frustrating thing with automated tests. Tests that fail time to time. I have whole section in this post about them. &#128556;</p><h2>The Shape of a good test</h2><p>Every test you write should have&nbsp;<a href="https://thoughtbot.com/blog/four-phase-test">four phases</a>.</p><ol><li><p><strong>Setup</strong>&nbsp;- Put SUD in a constant state, set data needed for the test</p></li><li><p><strong>Action</strong>&nbsp;- Perform the action we want to verify is doing what we expect it to do</p></li><li><p><strong>Verify</strong>&nbsp;- Verify that the result of the action is what we expect it to be</p></li><li><p><strong>Teardown</strong>&nbsp;- Puts SUD back to its initial state. This is often done automatically by testing libraries. Example: Resetting the database of all created records during the test.</p></li></ol><p>Example</p><pre><code>// test suit
describe(emojiForCountryCode.name, () =&gt; {
  // test case
  it('returns the country emoji for given country', () =&gt; {
    // no setup, because test doesn't depend on anything
    // action
    const value = emojiForCountryCode('BG');
    // verify
    expect(value).toEqual('&#127463;&#127468;');
    // no teardown was needed
  });
});</code></pre><p>Here is a similar example in Ruby:</p><pre><code>describe Post do 
  describe '#state' do
    it 'is scheduled when scheduled at a time in the future' do
      # setup - create post
      post = create :post, scheduled_at: 1.day.from_now
      
      # action and verify 
      # written on single line, because more readable
      expect(post.state).to eq 'scheduled'
      # teardown - cleans the database, done automatically
    end
  end
end</code></pre><p>All my tests in every language and testing framework follow this structure.</p><p>It is fine to have multiple assertions in the `verify` step. The goal is not to have `action verify action verify`, but not a single assertion.</p><p>So, the following is fine:</p><pre><code>describe Post do 
  describe '#schedule' do
    it 'marks post as scheduled' do
      # setup - create post
      post = create :post

      # action 
      post.schedule_to 1.day.from_now
      
      # verify the action by multiple assertions
      expect(post.state).to eq 'scheduled'
      expect(post.scheduled_at).to eq 1.day.from_now
    end
  end
end</code></pre><p>There is one group of tests where I don't follow the four phases of testing, - the E2E tests. There, I do multiple actions and verify that the action is completed before continuing to the main step.</p><pre><code>setup - create an unpublished blog post

action - visit post edit page
verify - the post is shown on the edit page

action - select new "published_at" date from date picker
verify - the correct date selected from the date picker

action - submit the form
verify - no errors and a success message is shown
verify - post's "published_at" is changed in the database

action - go to the index page
verify - the post is shown on the page
</code></pre><p>This is because E2E often fails "randomly," and we want to know where exactly something failed. In my example - imagine if I had a custom date picker and the JS broke for it. I didn't test the picker to set the correct value. Then, I get an error that the post has not been published. How much time am I going to lose trying to find this error?</p><p>Another thing I do with E2E tests, which is slightly controversial, is use a single test scenario for multiple related flows. This is because setup/teardown steps in E2E take too much time.</p><p>So, what I do is to test multiple things in E2E.</p><pre><code># I like to name my E2E tests like this
test "comment &gt; create &gt; edit &gt; destroy" do
  # test create comment with empty form
  # test create a comment with fill-in form
  # test edit of comment
  # test destroy of comment
end</code></pre><h2>Dealing with brittle tests</h2><p>This is one of the most frustrating experiences you can have as a developer. Some test randomly fails in CI. <em>You have worked hard on your PR, and when you merge it into the main branch, the test randomly fails</em>. &#128545;</p><p>In my experience, the main reasons for a test to fail "randomly"  are:</p><p><strong>1/ Time</strong></p><p>Often, this is because there is some dependency on time in our test or code. </p><p>For example, we expect <code>30.days.ago</code>, the month will be changed. However, if the month is 31 days or 28 days. A good practice is to freeze time with something like <a href="https://github.com/travisjeffery/timecop">Timecop</a> or isolate current time as a dependency.</p><p>Issues can arise when we compare time, too. </p><p>Example:</p><pre><code>now = Time.now
post = create :post
expect(post.created_at).to eq now // might be 1ms difference </code></pre><p><strong>2/ Leakage of state between tests</strong></p><p>In E2E tests running a browser, those can be - cookies, local stage, etc.</p><p>I once had a randomly failing E2E test because the previous tests set something in `localStorage`, and the current test was "assuming" localStorage was empty. &#129318;&#8205;&#9794;&#65039;</p><p>My tests in CI usually run in random order, so I had this failure when those two tests were run one after the other.</p><p>When you debug failing tests in CI with random order running, make sure locally you. Run tests in the same order, bypassing the random test order seed number.</p><p>Similar issues can arise when there are leftovers from the previous run</p><p>Fixing this is to make sure `setup` / `teardown` are strict.</p><p>E2E tests often suffer from those issues because cleaning after a test like this is often very complicated. </p><p><strong>3/ Async code</strong></p><p>This also affects E2E tests. <em>In general E2E test tend to be the brittle ones</em> &#129335;&#8205;&#9794;&#65039;. </p><p>Example: click a button and then assert something in database changes. However, you assert that the request is still being processed. </p><p>Fix 1: Use <code>wait_for_ajax</code> utility like <a href="https://gist.github.com/RStankov/5c9bb46f0a4e07a3a2e9af19e8738c51">this one</a>.<br>Fix 2: Fix before verifying in the database; check if page state is changed (this breaks with optimistic UI changes)</p><p>I usually do both things, <em>just&nbsp;in case.</em></p><p>Another example: Animation is running, so the element is not visible on the page</p><p>Fix 1: Disable animation<br>Fix 2: Have timeouts on your DOM assertions</p><p><strong>4/ Database order</strong></p><p>You make a query and expect post1, post2, and post3, but you get post2, post1, and post3.</p><p>The fix is to have an explicit order set in your query or to sort before verifying if you don't care about the order.</p><p><strong>5/ Network</strong></p><p>If your tests make external network requests, you will see &#8220;random&#8221; failures. The solution is to block all ongoing network requests in the SUD system.</p><p>Don't forget to turn off this in E2E browser tests as well.</p><h2>Ruby on Rails testing tips</h2><p>I prefer&nbsp;<a href="https://rspec.info/">RSpec</a>&nbsp;over mini-test. <em>I can write a whole post about it.</em></p><p>Most of my tips can be reused with other libraries as well.</p><h3><strong>What should have a test in Ruby on Rails project?</strong></h3><ul><li><p>Always test </p><ul><li><p>The "<em>Active</em>" part of Rails - ActiveJob, ActiveRecord, ActiveModel</p></li><li><p>GraphQL mutations and resolvers classes</p><ul><li><p>If the GraphQL type method gets complex, move to the resolver and test resolver </p></li></ul></li><li><p>Utilities, Service, Search, Form, and Value objects.</p></li></ul></li><li><p>Don't test</p><ul><li><p>The "Action" part of Rails - ActionController, ActionViews, ActonMailer, &#8230;</p></li><li><p><a href="https://viewcomponent.org/">ViewComponents</a> (<em>for those previews are sufficient, imho</em>)</p></li><li><p>DLS based code with something like <a href="https://activeadmin.info/">ActiveAdmin</a></p><ul><li><p>if you have complicated logic, move it out and test it</p></li></ul></li><li><p>private methods</p><ul><li><p>private objects who are already tested by the interface of the object using them</p></li></ul></li></ul></li><li><p>Write previews</p><ul><li><p>Action Mailer <a href="https://guides.rubyonrails.org/action_mailer_basics.html#previewing-emails">Previews</a></p></li><li><p>View Components <a href="https://viewcomponent.org/guide/previews.html">Previews</a></p></li><li><p><em>You can write a smoke test</em>&nbsp;that opens a browse, opens all preview pages, and makes sure nothing blows up &#128165;</p></li></ul><p></p></li></ul><h3><strong>Tips for handling context/describe/it nesting</strong></h3><p>Use <code>.class_method</code> for class methods and <code>#instance_method</code> instance method naming. It helps a lot when when reading the output.</p><pre><code>describe User do
  # test class method User.find_and_authorize
  describe ".find_and_authorize"

  # test instance method User.new.show_cookie_banner?
  describe "#show_cookie_banner?"
end</code></pre><p>Don't have a single `it` in multiple `context`/`describe`</p><pre><code>describe Object do
  describe '.class_method' do
    context 'a' do 
      context 'b' do 
        it 'does something'
       end
       
       context 'c' do 
        it 'does something'
       end
    end
  end
end

# convert to

describe Object do
  describe '.class_method' do
    it 'does something when a and b'
    it 'does something when a and c'
  end
end</code></pre><p>It is fine to have <code>describe</code> for each method of object</p><pre><code>describe Object do
  describe '.class_method' do
    it 'does something'
  end

  describe '#instance_method' do
    it 'does something as well'
  end
end</code></pre><h3><strong>Tips for using "let"</strong></h3><p>Avoid overusing &#8220;let&#8221;. The key &#8220;<strong>overusing</strong>&#8221;, it is fine to it time to time. These are my rules:</p><ul><li><p>Use <code>let</code> when the object global for test and doesn't need configuration like <code>user</code> or <code>account</code>. Basically objects you don&#8217;t care about much</p></li><li><p>If any of the <code>let </code>objects need to be modified in &#8220;setup&#8221; phase, replace <code>let </code>with factory method that creates whats needed and have params.</p></li><li><p>If you have more than 2 lets (and especially if they are calling each other), consider inlining them and using factory methods</p></li><li><p>Don&#8217;t overwrite <code>let</code> between <code>describe</code> blocks</p></li><li><p>Don't ever use <code>let!</code> it creates a lot of <a href="https://thoughtbot.com/blog/mystery-guest">mystery guest</a> issues</p></li></ul><p><a href="https://thoughtbot.com/blog/lets-not">Here</a> and <a href="https://thoughtbot.com/blog/my-issues-with-let">here</a> are good articles illustrating issues with &#8220;let&#8221;, so use with cation.</p><h3><strong>General <a href="https://rspec.info/">RSpec</a> tips</strong></h3><ul><li><p>Use the <code>expect(value).to eq true</code> syntax.</p></li><li><p>Use&nbsp;<a href="https://rubydoc.info/gems/rspec-expectations/RSpec%2FMatchers:have_attributes">have_attributes</a>&nbsp;matcher for asserting multiple attributes per object</p></li><li><p>Never use&nbsp;<a href="https://rspec.info/features/3-12/rspec-core/subject/explicit-subject/">subject</a>&nbsp;- it is very implicit and makes tests hard to follow.</p></li><li><p>Define custom&nbsp;<a href="https://rspec.info/documentation/3.0/rspec-expectations/RSpec/Matchers">matches</a>&nbsp;for common assertions, especially for supporting domain functionality. </p><ul><li><p>Example: expect(user).to have_received_notification_about(post)</p></li></ul></li><li><p>Use&nbsp;<a href="https://relishapp.com/rspec/rspec-core/docs/example-groups/shared-examples">shared examples</a>&nbsp;if you want to verify object from a certain type behaves as an object of this type; good example are&nbsp;<a href="https://api.rubyonrails.org/v7.1.2/classes/ActiveSupport/Concern.html">concerns</a> behaviors</p></li></ul><pre><code>describe Post do
  it_behaves_like 'votable', factory: post
end</code></pre><h3><strong>Tips for using Factories</strong></h3><p>I like using&nbsp;<a href="https://github.com/thoughtbot/factory_bot">FactoryBot</a>, which creates records for testing.</p><ul><li><p><strong>Every database model should have a factory defined for</strong></p></li><li><p>Factories should be bare minimal (<a href="https://thoughtbot.com/blog/factories-should-be-the-bare-minimum">article</a>)</p></li><li><p>Use traits to create variants of factories (<a href="https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md#traits">docs</a>)</p><ul><li><p>If you notice in a lot of tests, you do orchestration of 2-3 records to set factories &#8594; extract trait</p></li></ul></li><li><p>Include  use &#8220;config.include FactoryBot::Syntax::Methods &#8220; in your configration</p><ul><li><p>this gives &#8220;build&#8221; and &#8220;create&#8221; factory creation methods</p></li></ul></li></ul><p>One big issue with factories is that they often create a lot of background records for a single record. </p><p>An example is a system with products and categories. Creating a simple product will also create a category, maybe users, an account where this user belongs... etc. You get the point. </p><p>I have found a couple of strategies to deal with this.</p><ol><li><p>Use &#8220;build&#8221; instead &#8220;create&#8221; where possible</p></li><li><p>Define the minimum amount of association in default factories. Everything that the database allow nullable columns</p></li><li><p>Re-use records from similar relationships. </p></li></ol><pre><code>FactoryBot.define do
  factory :money_transfer do
     association :user
     association :building
     association :source, factory: :withdraw
  end
end</code></pre><p>This will create 1 user, 1 building, 1 withdraw and 1 user and 1 building for the withdraw: Total: 5</p><pre><code>FactoryBot.define do
  factory :money_transfer do
     association :source, factory: :withdraw
     user { source.user }
     building { source.building }
  end
end</code></pre><p>This will create the minimum:  1 user, 1 building, 1 withdraw.</p><h2>End-to-End (E2E) testing tips</h2><p>I find a lot of value in those tests. </p><p>E2E tests are the best storytellers of what your system <em>actually</em> does.</p><p>Often, system breaks and bugs spear in the integration between layers. So having a test that tests a feature from UI to through the backend to the database and email delivery schedule is very useful.</p><p>My main goal of the E2E test is to verify</p><ol><li><p>Layers of my system work.</p></li><li><p>Help me with big refactoring when I change a lot in database models and logic, but UI and flows should keep working</p></li></ol><p>People don't write those tests because they are slow (<em>to write and run</em>) and often brittle. <br></p><p>Most of the problems are about tooling around writing feature tests.</p><p>Good tooling is your saver here. In almost every project I worked on in the last decade, I had a lot of tooling built around making E2E easier. </p><p>To have success with E2E tests, you need to optimize your tools for</p><ol><li><p>speed of writing </p></li><li><p>easy debug when they break.</p></li></ol><p>Most of my advice here will be about <a href="https://rspec.info/">RSpec</a> and <a href="https://github.com/teamcapybara/capybara">Capybara</a>. <em>But again, a lot of my tips work with other technologies.</em></p><h3><strong>Tip 1: Proper configuration</strong></h3><pre><code>RSpec.configure do |config|
  # resize the browser before every test
  # often, tests fail because UI pushes elements out of the viewport
  # if you need to test desktop / mobile sizes, you can use "it" tags  
  config.before(:each, type: :feature) do
    Capybara
      .current_session 
      .driver
      .browser.manage
      .window
      .resize_to(2_000, 2_000)
  end

  # when there is a failed test
  # 1. make and save screenshot of the page
  # 2. print page url and text
  # can't tell you how many hours of debugging those have saved me
  config.after(:each, type: :feature) do |example|
    if example.exception.present?
      timestamp = Time.zone.now.strftime('%Y_%m_%d-%H_%M_%S')
      filename = "#{timestamp}__#{example.full_description}.png"
      screenshot_path = Rails.root.join('tmp', 'screenshots', filename)

      puts "Saving screenshot: #{screenshot_path}"
      Capybara.page.save_screenshot(screenshot_path)

      if Capybara.page.current_url.present?
        puts ''
        puts 'PAGE URL: '
        puts Capybara.page.current_url
        puts 'PAGE TEXT: '
        puts Capybara.page.text
      end
    end
  end
end</code></pre><h3><strong>Tip 2: Write the test interactively</strong></h3><p>When I need to write a complicated feature spec, I do the following:</p><ol><li><p>Create an empty test case</p></li><li><p>Call the needed factory to set the database</p></li><li><p>Put a "debug" statement</p></li><li><p>Run the tests</p></li><li><p>Then, in the interactive console</p><ul><li><p>I write the code step by step to verify the feature.</p></li><li><p>When I'm happy with the step code, I paste in the test</p></li><li><p>Exit when I'm happy with the result</p></li></ul></li><li><p>Run the test with pasted code</p></li><li><p>Clean the test</p></li><li><p>Move to next task</p></li></ol><h3><strong>Tip 3: Use data attributes for matching test elements</strong></h3><pre><code>// instead of this
element.find('.description button.expand-button').simulate('click');

// write this
element.find('[data-test="test"]').simulate('click');</code></pre><p>This clarifies what is used for testing and which is part of UI. In this way, a simple UI redesign doesn't break the test.</p><h3><strong>Tip 4: Wait for ajax</strong></h3><p>The big cause for brittle tests is to have a UI action that makes an Ajax call, where you check for Ajax call results before the call has been processed.</p><p>Because of this, in all my projects, I have a method named <code>wait_for_ajax</code>; it is always custom for every application.</p><p>It waits for ongoing Ajax calls to be done before continuing the test.</p><p>Here is a <a href="https://gist.github.com/RStankov/5c9bb46f0a4e07a3a2e9af19e8738c51">wait_for_ajax</a>, I used with React application using GraphQL.</p><h3><strong>Tip 5: Write helpers for custom operations</strong></h3><p>In E2E, you often have to perform a group of related operations. It is useful to have shared helpers for those. </p><p>For example, if you have a fancy calendar picker.  Every time you use it, you will have to write the following:</p><pre><code>- click data-test="calendar-picker"
- enter the correct year in a text input
- pick a correct month from month picker
- click on the correct day
- click the "ok" button to save</code></pre><p>Having a single helper function makes a lot more sense, so when you change "ok" with "done", you can do it in one place.</p><pre><code>select_calendar_picker(date)</code></pre><p>Here are some other examples for grouping operations:</p><pre><code>login_as(user)
enter_comment_with(text)
submit_comment_with(text)
close_modal()
enter_in_from(values)</code></pre><p>You can hide your `<code>verify</code>` checks in those helpers.</p><p>This is essential to have a maintainable E2E test suite. Ideally, a good test should read like a story, and its implementation details would be hidden.</p><p>One of my favorite helpers is `<code>submit_form</code>` (I write it customarily on every project because forms and custom form elements differ between projects):</p><pre><code>```ruby

# regular capybara with helpers for custom inputs
fill_in 'Name', 'Rado'
fill_in_custom_calendar_picker 'Date label', date
check 'Verify'
select_from_custom_select 'Select', 'value'

# helper that hides all forms interactions
submit_form(
  name: 'Rado',
  date: date,
  verify: true,
  select: 'value'
)
```</code></pre><p>Here is an example of an E2E test from&nbsp;&nbsp;<a href="https://angrybuilding.com/">Angry Building</a>. It tests the&nbsp;<a href="https://en.wikipedia.org/wiki/Create,_read,_update_and_delete">CRUD</a>&nbsp;of our issue-type module.&nbsp;</p><pre><code>feature "Issue Module" do
  scenario 'types &gt; new &gt; list &gt; update &gt; destroy' do
    building = create :building

    sign_in_operator(building)

    visit account_issues_path(building.account)

    # test: create issue type
    click_on I18n.t(:page_title_issue_types)

    click_on I18n.t(:action_new_issue_type)

    submit_form

    expect_form_errors :blank

    # NOTICE this helper
    # it will raise error if there is validation error
    # name - is string
    # severity - is select
    # visible_in_app - is checkbox
    submit_form!(
      name: 'New Issue Type',
      severity: 'warning',
      visible_in_app: false,
    )

    expect_flash_message :create

    expect(page).to have_content 'New Issue Type'

    type = building.account.issue_types.first!

    expect(type).to have_attributes(
      name: 'New type',
      severity: 'warning',
      visible_in_app: false,
    )

    # test: update issue type
    click_on_edit

    submit_form(
      name: 'Updated Issue Type',
      severity: 'low',
      visible_in_app: true,
    )

    expect_flash_message :update

    expect(page).to have_content 'Updated Issue Type'

    type.reload

    expect(type).to have_attributes(
      name: 'Updated Issue Type',
      severity: 'low',
      visible_in_app: true,
    )

    # test: destroy issue type (without related issues)
    issue = create :issue, building: building, type: type

    click_on_destroy

    expect_to_be_destroyed type
    expect_not_to_be_destroyed issue
  end
end
</code></pre><h2>JavaScript testing tips</h2><p><em>Again</em>, many of my tips (<em>especially the E2E ones</em>) apply for other environments.</p><p>As mentioned, testing JavaScript code is not as fun as testing Ruby. This is mostly due to tooling. <a href="https://jestjs.io/">Jest</a> is good, but it can't compare with RSpec. </p><p>The code in most npm packages isn't designed with testing in mind. In JavaScript project&#8217;s, often you write a lot of glue code to connect many tiny packages.</p><p>How I test JavaScript depends on whether I use JavaScript for frontend or backend.</p><ul><li><p>If JavaScript is used for backend</p><ul><li><p>I do a lot more automated testing with rules similar to what I do with Ruby.</p></li><li><p>I try to isolate and not test external dependencies.</p></li></ul></li><li><p>If  JavaScript is used for frontend</p><ul><li><p>I mostly test pure utility function </p><ul><li><p>The heavy lift of testing is done vie E2E tests</p></li></ul></li><li><p>In React, I sometimes just test hooks</p><ul><li><p>I do "<a href="https://blog.rstankov.com/extract-react-hook-refactoring/">Extract hook refactoring</a>"  and then <a href="https://speakerdeck.com/rstankov/testing-react-hooks-with-confidence">test the hook</a> (<a href="https://www.youtube.com/watch?v=I9dorcro__s&amp;t=1s&amp;ab_channel=JavaScriptConferencesbyGitNation">video</a>, <a href="https://github.com/RStankov/talks-code/tree/master/2021.01.29%20-%20Testing%20React%20Hooks%20with%20Confidence">code</a>)</p></li></ul></li></ul></li></ul><p>On tip: when testing with JavaScript, don't hard code names of functions:</p><pre><code><code>// if you change the function name you don't need to change the test
// bonus, you know naming your function works.
describe(emojiForCountryCode.name, () =&gt; {
  // ... 
});</code></code></pre><p><strong>If you use TypeScript, do you still need tests?</strong>  &#129300;</p><p>TypeScript is like a smoke test, making sure your functions are called with the functions, the right arguments, and components are integrated. It helps a lot with refactoring. </p><p>It is still useful to have tests for more complicated logic and E2E tests</p><h2>Conclusion</h2><p>I can write a lot more on the subject of testing. However, this is getting a bit too log. &#128517;</p><p>People don't write automated tests due to a lack of tooling and not knowing how to write tests. I hope that these tips have helped you on both fronts.  </p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading. Subscribe for free to receive more tips and support my work. &#128588;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>If you have any questions or comments, you can ping me on&nbsp;<a href="https://www.threads.net/@rstankov">Threads</a>,&nbsp;<a href="https://www.linkedin.com/in/radoslavstankov/">LinkedIn</a>,&nbsp;<a href="https://mastodon.social/@rstankov">Mastodon</a>, <a href="https://twitter.com/rstankov">Twitter</a> or just leave a comment below &#128237;</p>]]></content:encoded></item><item><title><![CDATA[The Evolution of Angry Building Export Module]]></title><description><![CDATA[The post follows the timeline of the decision and the refactorings I did toward the current version of the Export module of Angry Building. It explains how I extract Service objects, Namespacing, and Value objects and integrate them with Ruby on Rails.]]></description><link>https://tips.rstankov.com/p/the-evolution-of-angry-building-export</link><guid isPermaLink="false">https://tips.rstankov.com/p/the-evolution-of-angry-building-export</guid><dc:creator><![CDATA[Radoslav Stankov]]></dc:creator><pubDate>Tue, 16 Jan 2024 20:48:25 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/f9165a81-7b49-4a5a-b414-a3c7f9deeaa1_1024x732.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey &#128075;</p><p><a href="https://angrybuilding.com/">Angry Building</a> (the company I co-founded) helps manage a facility's finances and documents. This involves having the ability to export data out of the system in various formats. Our <code>Export</code> module handles those exports. </p><p>This module's structure is a good illustration of several useful patterns and techniques for modern web applications.</p><p>In this post, I will follow the timeline of the decision and the refactorings I did toward the current version of the Export module.</p><p><a href="https://angrybuilding.com/">Angry Building</a> uses <a href="https://rubyonrails.org/">Ruby on Rails</a>, so all examples are in Ruby.</p><p>If you are not using <a href="https://rubyonrails.org/">Ruby on Rails</a>, the techniques described in this post generally apply - see the "Outside of Ruby on Rails" section of this post</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://tips.rstankov.com/subscribe?"><span>Subscribe now</span></a></p><h2>The Setup</h2><p>My initial set of export-related features were:</p><ul><li><p>Generate a PDF of an invoice to be shared with the tenant.</p></li><li><p>Export CSV of generated invoices to be imported into a 3rd party. Accounting software name Plus Minus (<em>Used by our first major client</em>).</p></li><li><p>Generate an Excel file of unpaid taxes of a tenant; this is used as evidence for legal cases against debtors.</p></li><li><p>Download a zip archive of all files attached to the building.</p></li><li><p><em>...and a lot more similar like this.</em></p></li></ul><p>As with every rapidly developed product, those weren&#8217;t grouped as an single export feature. They came as sub-tasks of other features, I was working on.</p><h2>Step 1 - Service object</h2><p>The first pattern I used is what we call a "Service Object" in the Ruby world. Another name for this pattern is a "<a href="https://martinfowler.com/eaaCatalog/transactionScript.html">Transaction Script</a>". <em>This is the term, I prefer because "Service Object" is often mistaken for microservice</em> &#129335;&#8205;&#9794;&#65039;</p><p>"Service objects" are Plain Old Ruby Objects (PORO) designed to execute one single action in your domain logic. They help with code organization and make the code easier to test.&nbsp;</p><ul><li><p>I name those service objects as verbs, because they represent actions. </p><ul><li><p>I&#8217;m fine with long and descriptive names because they are easy to search across the system.</p></li><li><p>Examples: <code>GeneratePaymentInvoice</code> or <code>ChangePaymentAmount</code>.</p></li></ul></li><li><p>Most of my service objects are modules with <code>extend self</code> and a method named <code>call</code>, which is the entry point of the action.&nbsp;</p><ul><li><p>When the <code>call</code> method gets too big, I extract the private methods to increase its readability.</p></li></ul></li><li><p>The service objects follow a procedural coding style.</p><ul><li><p>It is fine for me the implementation of a service object to be a bit messy, as long as is covered by tests and input / output is clear.</p></li></ul></li></ul><p>Here is an example of the service object the "Generate an Excel file of unpaid taxes" feature:</p><pre><code>module ExportUnpaidTaxesToExcel
  extend self

  def call(taxes)
    # ... logic
    
    excel_file
  end 

  private

  # ... helper methods
end</code></pre><p> Because they have simple input, clear output and obvious path of execution. Service object are easy to test and read.</p><p>In some cases, a module will have many shared arguments that must be passed around its private methods. In those cases,  I use a class masquerading as an <code>extend self module</code>.</p><pre><code>class GenerateInvoiceDocumentPdf
  # This is what the consumers of this service are going to use.
  # Everything below this is an "private" implementation detail.
  def self.call(document)
    new(document).call
  end

  attr_reader :document, :pdf

  def intialize(document)
    @document = document
    @pdf = Prawn::Document.new(page_size: 'LETTER')
  end
  
  def call 
    watermark
    print_header
    print_company_and_customer
    print_line_items
    print_payment_info
    print_powered_by
   
    pdf
  end

  private

  # ... helper methods
end</code></pre><p>In this example, I use a class to <code>document</code> and <code>pdf</code> attributes in every helper method without passing them around.</p><p>Sometimes, I use class to memorize some of its methods.</p><p>I use this style cautiously because it can lead to having too many instance variables and short private methods. Then, you have to do a lot of reading back and forth to follow the logic.</p><p>Here is an example:</p><pre><code>def call
  if some_condition?
    do_something
  else
    do_some_other_thing
  end
end


def some_condition?
  some_other_condition? || some_third_condition?
end

def some_other_condition?
  @record1.some_other_condition?
end

def some_third_condition?
  @record2.some_third_condition?
end</code></pre><p>I call this the "<em>pretend to be an interpreter</em>" game. To understand a code like this, you must interpret the code and keep a stack in your head. &#128565;</p><p><strong>The key here</strong>&nbsp;is that both <code>extend self module</code> and <code>class</code> behave in the same way for the outside user of the code.&nbsp;</p><p>I have had many cases where a single service object has switched styles multiple times during development, often without needing to change their tests.</p><h2>Step 2 - Namespacing</h2><p>One of the strategies that help me manage a large Ruby on Rails codebase is to use modules as namespaces very widely. I use namespace for every domain and group of utilities. I often move objects/namespaces around as the system evolves.</p><p>Originally, I was putting the export service objects inside of the namespace of the domain they were related to. Following the <a href="https://www.oreilly.com/library/view/software-design-x-rays/9781680505795/f_0031.xhtml">Proximity Principle</a> and my interpretation - "<em>code that is related should be closer together</em>".</p><ul><li><p>PDF of the invoice, to be shared with tenant &#8594; <code>Invoicing::GeneratePdf</code></p></li><li><p>CSV export to PlusMinus &#8594; <code>Invoicing::ExportToPlusMinus</code></p></li><li><p>Excel file of unpaid taxes of a tenant &#8594; <code>Taxation::DebtorsLegalExport</code></p></li><li><p>Download a zip archive of all files attached to building &#8594; <code>Buildings::DownloadNotesZip</code></p></li></ul><p>This worked for a while. However, I had mistaken what is "related" in this case. An exporter is more related to other exporters compared to what it is exporting. &#128565;&#8205;&#128171;</p><p>All exporters should be grouped in the <code>Export</code> namespace.</p><p>In Domain Drive Design terms, the <code>Export</code> module is a "<a href="https://www.goodreads.com/book/show/57573212-learning-domain-driven-design">Supporting sub-domain</a>".</p><p><em>How did I get to this conclusion?</em></p><p>When creating a new exporter, I almost always copy an existing exporter and adjust it to fit the new feature. I was wasting time searching for exporters to copy. &#128584;</p><p>This will get even harder when I start to hire developers to work with me at Angry Building. </p><p>An even bigger issue with this structure was that spotting code for extraction and refactoring was hard because objects were spread across multiple directories and not grouped.</p><p>All PDF exporters following the same structure was not an accident of me copying/pasting. It was because the logic for generating PDFs is similar. </p><p>It is the same for Zip, Excel, CSV, etc. But I couldn't see this when the code was spread around.</p><p>I decided to create the domain namespace module named <code>Export</code> (<em>very clever name, I know</em>). I moved all service objects to the new namespace:</p><ul><li><p><code>Export::InvoiceToPdf</code></p></li><li><p><code>Export::InvoicesToPlusMinusCsv</code></p></li><li><p><code>Export::DebtorsLegalExportExcel</code></p></li><li><p><code>Export::NotesToZip</code></p></li></ul><p>I noticed how messy the naming was when I saw all service objects in the same directory. When I name stuff, I often try to follow predictable patterns like </p><p><code>[namespace][object][action]</code> or <code>[namespace][thing][type]</code> or etc. This creates symmetry when you list files in a directory or have an auto-complete suggestion in your editor. </p><p>In this case I decided to go with [namespace]::[export type]::[what is exported] :</p><ul><li><p><code>Export::Pdf::Invoice</code></p></li><li><p><code>Export::Csv::PlusMinus</code></p></li><li><p><code>Export::Excel::Debtors</code></p></li><li><p><code>Export::Zip::Notes</code></p></li></ul><p>Having those extra modules for <code>Pdf</code> / <code>Csv</code> / <code>Excel</code> / <code>Zip</code> modules gives me the following benefits:</p><ol><li><p>It was easy to go through all related exporters and notice opportunities for refactoring</p></li><li><p>I had an obvious place to put shared utilities between each exporter</p></li></ol><p>Example: I noticed that all PDFs were using the&nbsp; <a href="https://en.wikipedia.org/wiki/A4">A4</a> paper format, so I created an Export::Pdf.a4 which returns a Prawn::Document (the library that I use for PDFs)&nbsp; instance with A4 size.</p><h3>Step 3 - Value object</h3><p>Next, I noticed that I often copied code in my <a href="https://guides.rubyonrails.org/action_controller_overview.html">controllers</a> that sent the files to the user:</p><pre><code>file = Export::Csv::PlusMinus.call(invoices)

send_data(
  file, 
  type: 'text/csv', 
  disposition: 'attachment', 
  filename: 'plus_minus.csv'
)</code></pre><p>This pushed me to exact a <a href="https://martinfowler.com/bliki/ValueObject.html">value object</a> named <code>Export::File.</code></p><pre><code>class Export::File
  attr_reader :filename, :type, :content, :disposition

  class &lt;&lt; self
    # I was considering having classes for each file type.
    # Then I decided that having couple of factory methods is cleaner.
    def csv(name, csv_content)
      new("#{name}.csv", 'text/csv', csv_content)
    end

    # ... similar method for PDF, Excel, Zip and etc
  end

  def initialize(filename:, type:, content:, disposition: :attachment)
    @filename = filename
    @type = type
    @content = content
    @disposition = disposition
  end
end</code></pre><p>Every exporter service object now returns this value object:</p><pre><code>module Export::Csv::PlusMinus
  extend self

  def call(documents)
    # ... logic
    
    Export::File.csv('plus_minus', content)
  end 
end</code></pre><p>The controller code became:</p><pre><code>file = Export::Csv::PlusMinus.call(invoices)

send_data(
  file, 
  type: file.content_type, 
  disposition: file.disposition, 
  filename: file.filename
)</code></pre><p>This push me to extract a simple controller helper method:</p><pre><code>file = Export::Csv::PlusMinus.call(invoices)
download_export(file)

# ... in ApplicationController
def download_export(export_file)
  send_data(
    file, 
    type: file.content_type, 
    disposition: file.disposition, 
    filename: file.filename
  )
end</code></pre><p>The value object can also be integrated with <a href="https://guides.rubyonrails.org/active_storage_overview.html">Active Storage</a>:</p><pre><code>file = Export::Pdf::Invoice.call(document)
document.file.attach(file)</code></pre><h2>Integration</h2><p>Rails has this&nbsp;<a href="https://edgeguides.rubyonrails.org/action_controller_overview.html#rendering">format feature</a>, which allows a controller action to respond differently depending on the requested file format.&nbsp;&nbsp;</p><p>I use this to the full extent. Here is what my invoices controller looks like:</p><pre><code>class InvoicingDocumentsController &lt; ApplicationController
  def index
    # Find an account that current user has access to.
    # I'll write about this authorized strategy in a future post.
    account = find_record Account, params[:account_id], authorize: :view

    # Search for documents based on search params
    # I'll write about SearchObject in a future post.
    @search = Invocing::DocumentsSearch.new(account, params)

    # URL: /account/[account_id]/invocing_documents.[format]
    # Depending on the URL's file format return HTML/ZIP/XLS/PDF/CSV.
    respond_to do |format|
      format.html
      format.zip do
        download_export(Export::Zip::Invoices.call(@search.all))
      end
      format.xls do
        download_export(Export::Excel::Invoices.call(@search.all))
      end
      format.pdf do
        download_export(Export::Pdf::InvoicesCombined.call(@search.all))
      end
      format.csv do
        download_export(Export::Csv::PlusMinus.call(@search.all))
      end
    end
  end
end</code></pre><p>In the UI, I have a filter form for invoices, and when the user has filtered down to the invoices they want to export. </p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4MY6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67169397-5acc-40d2-95e1-721fef341881_2696x614.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4MY6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67169397-5acc-40d2-95e1-721fef341881_2696x614.png 424w, https://substackcdn.com/image/fetch/$s_!4MY6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67169397-5acc-40d2-95e1-721fef341881_2696x614.png 848w, https://substackcdn.com/image/fetch/$s_!4MY6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67169397-5acc-40d2-95e1-721fef341881_2696x614.png 1272w, https://substackcdn.com/image/fetch/$s_!4MY6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67169397-5acc-40d2-95e1-721fef341881_2696x614.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4MY6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67169397-5acc-40d2-95e1-721fef341881_2696x614.png" width="1456" height="332" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/67169397-5acc-40d2-95e1-721fef341881_2696x614.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:332,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:203584,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4MY6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67169397-5acc-40d2-95e1-721fef341881_2696x614.png 424w, https://substackcdn.com/image/fetch/$s_!4MY6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67169397-5acc-40d2-95e1-721fef341881_2696x614.png 848w, https://substackcdn.com/image/fetch/$s_!4MY6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67169397-5acc-40d2-95e1-721fef341881_2696x614.png 1272w, https://substackcdn.com/image/fetch/$s_!4MY6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67169397-5acc-40d2-95e1-721fef341881_2696x614.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The ViewComponent for the menu, just".format" to the URL and return what was selected.</p><pre><code>&lt;%= render Action&#1052;enuComponent.new(title: :download) do |menu| %&gt;
  &lt;% menu.action :button_zip, url(@search.account, format: :zip, **@search.params) %&gt;
  &lt;% menu.action :button_plus_minus, account_invoicing_documents_path(@search.account, format: :csv, **@search.params) %&gt;
  &lt;% menu.action :button_excel, account_invoicing_documents_path(@search.account, format: :xls, **@search.params) %&gt;
  &lt;% menu.action :button_pdf, account_invoicing_documents_path(@search.account, format: :pdf, **@search.params) %&gt;
&lt;% end %&gt;</code></pre><h2>Future expansion</h2><p>Work is never done. </p><p>A couple of features in the Angry Building roadmap will involve moving the file exporters to be performed in a background job because exporting more data on demand in a web request is not a good idea. I have a couple of ideas on how to morph the structure to handle this case. I'll sure write a follow-up post when this happens.</p><p>I use libraries directly in the service object to generate PDF and Excel files. I might create wrappers around those and use the builder pattern to make the exporters themselves simpler. Will see when I get there.</p><h2>Outside of Ruby on Rails</h2><p>At first sight, what I have described so far looks very Ruby on Rails-specific. &#129320;</p><p>However, I have also used all the techniques from above in Node.js projects. &#129488;</p><p>Let's imagine I have used Node.js  and TypeScript for AngryBuilding. What will the export system look like? &#129300;</p><ul><li><p>I will structure the directories as I did with the namespaces.</p></li><li><p>All exporters will be functions instead of service objects;</p></li><li><p>The file value object will be TypeScript type, created via factory methods.</p></li></ul><p>Here is what the directory will look like:</p><pre><code>lib/export/index.ts
lib/export/file.ts - factories for value object
lib/export/pdf/utils.ts - all utils for PDF will be
lib/export/pdf/invoice.ts - an exporter
lib/export/excel/utils.ts - all utils for excel will be
lib/export/excel/invoices.ts - an exporter</code></pre><p>Here is how the utilities going to look like:</p><pre><code>// lib/export/pdf/utils

// Forward those so exporters have fewer imports
export { filePdf, IFile } from '../file'; 

export function a4() {
   // ...
}

// ...</code></pre><p>The exporter will look like something like this:</p><pre><code>// lib/export/pdf/invoices
import { a4, filePdf, IFile } from './utils';

// IFile is optional since TypeScript can infer
export default function(document): IFile { 
 
   const content = a4();

   // ...

   return filePdf(`document-${document}`, content);
}</code></pre><p>The exporter can used as this:</p><pre><code>import generateInvoice from '@/lib/export/pdf/invoice'</code></pre><p><em>As you can see the structure looks very similar.</em></p><p>Most of the differences will be in the glue of the system - libraries used to generate the various formats and the code sending them to the user.</p><h2><strong>Conclusion&nbsp;</strong></h2><p>The Export modules is a good illustration, how I think about software design and system evolution .</p><p>In this post, I have outlined some the essential patterns for a modern Ruby on Rails application:</p><ul><li><p>Service objects</p></li><li><p>Namespaces</p></li><li><p>Value objects</p></li></ul><p>The patterns, shared can be used not only in Ruby on Rails projects.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://tips.rstankov.com/subscribe?"><span>Subscribe now</span></a></p><p>p.s. I&#8217;m experimenting with companion videos for my posts.&nbsp;</p><p><em>I hope you find them useful.</em>&#129310;</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;355ebdaa-b588-4ac8-ac39-c1228c323d83&quot;,&quot;duration&quot;:null}"></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading. Subscribe for free to receive more tips and support my work. &#128588;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>If you have any questions or comments, you can ping me on&nbsp;<a href="https://www.threads.net/@rstankov">Threads</a>,&nbsp;<a href="https://www.linkedin.com/in/radoslavstankov/">LinkedIn</a>,&nbsp;<a href="https://mastodon.social/@rstankov">Mastodon</a>, <a href="https://twitter.com/rstankov">Twitter</a> or just leave a comment below &#128237;</p><p></p>]]></content:encoded></item><item><title><![CDATA[Newsletter Init]]></title><description><![CDATA[Welcome to my new newsletter &#128588;]]></description><link>https://tips.rstankov.com/p/newsletter-init</link><guid isPermaLink="false">https://tips.rstankov.com/p/newsletter-init</guid><dc:creator><![CDATA[Radoslav Stankov]]></dc:creator><pubDate>Sat, 06 Jan 2024 21:34:30 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/2291debf-d496-4c6e-a1ca-c9f3fdf7ddf7_1024x1024.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey, I'm Rado &#128075;</p><p>Welcome to my Substack newsletter - <a href="https://tips.rstankov.com/">Rado's Tips</a>.</p><p>Since this is #1 post of the newsletter, if you are reading this:</p><p>a) you are a friend who I nagged to subscribe<br>b) you will be a friend because you subscribed on your own<br>c) you don&#8217;t know me and randomly arrived here.</p><p>In all cases, thank you &#128591;<br><br>A bit about me:<br><br>I'm the CTO and Co-Founder at <a href="https://angrybuilding.com/en">Angry Building</a> &#128293;, SaaS for facility management and neighbour communication platform (<em>with the goal to rebrand as HappyBuilding </em>&#128584;).<br><br>Previously, I was Head of Engineering at <a href="https://www.producthunt.com/">Product Hunt</a> &#128570;.</p><p>I have been working in tech since 2002. My first tech job started as a joke when I was 15, <a href="https://topenddevs.com/podcasts/my-ruby-story/episodes/mrs-077-radoslav-stankov">it is funny story</a> &#128517;.</p><p>This newsletter is an experiment, and I still don't know what the format will be. I will figure it out as I go. </p><p>I'd be happy if you joined me on this journey. </p><p>I plan to write at least once a week. I want to keep this practical and focus on sharing showcases from my experience. </p><p>Some of the posts will have a video explainer, which I'll post on <a href="https://www.youtube.com/@Rstankov">YouTube</a>. </p><p>I have been inspired by other newsletters like <a href="https://refactoring.fm/">Refactoring</a> and  <a href="https://newsletter.pragmaticengineer.com/">The Pragmatic Engineer</a>.</p><p>My writing style includes extensively </p><ul><li><p>numbered and bullet-point lists</p></li><li><p>emojis, gifs &#128517;</p></li><li><p>code samples</p></li><li><p>diagrams</p></li><li><p>step by step think process walkthroughs</p></li></ul><p></p><p><strong>Why I'm starting this Substack?</strong> &#129300;</p><ol><li><p>I'm often asked for advice and have a handy note ready to share. A central place to keep and link to my tips will be handy. </p></li><li><p>This will force the function to group my notes and thoughts into more coherent ideas.</p><p></p></li></ol><p><strong>Why am I not just using my existing blog?</strong> &#129300;</p><ol><li><p>With a newsletter, you push content to interested people; with a blog, you need to pull people to your content.</p></li><li><p>I'm using Substack instead to implementing newsletter capabilities to my existing blog because</p><ol><li><p> I want to deal with only some of the machinery of a newsletter system I manage. I have implemented the <a href="https://www.producthunt.com/newsletters/archive">Product Hunt Newsletter</a> delivery system and know how much work this involves. <em>[spoiler] I plan todo a detailed post about the design of that system in the future</em> &#129323;</p></li><li><p>Substack has a building reach tool that hopefully will help me grow this newsletter, motivating me to write more.</p></li></ol><p></p></li></ol><p>If you have any feedback for me you can leave a comment or ping me on&nbsp;<a href="https://www.threads.net/@rstankov">Threads</a>,&nbsp;<a href="https://www.linkedin.com/in/radoslavstankov/">LinkedIn</a>,&nbsp;<a href="https://mastodon.social/@rstankov">Mastodon</a>&nbsp;or&nbsp;<a href="https://twitter.com/rstankov">Twitter</a>. &#128237;</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://tips.rstankov.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading if you think my tips will be useful to you &#8594; subscribe to receive new posts</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><p>Before, I had a good back catalog - here are things I have written before and you might like:</p><ul><li><p><a href="https://blog.rstankov.com/collaborative-single-player-mode/">Collaborative Single Player Mode</a> - remote, process and management</p></li><li><p><a href="https://blog.rstankov.com/onboarding-software-engineers-remote-at-product-hunt/">Remote Onboarding</a> - remote, process and management</p></li><li><p><a href="https://blog.rstankov.com/product-hunt-architecture/">Product Hunt Architecture</a> - showcase and software architecture</p></li><li><p><a href="https://blog.rstankov.com/my-manager-journal/">Manager Journal</a> - management and productivity</p></li><li><p><a href="https://blog.rstankov.com/four-tips-for-end-to-end-testing/">Tips for End-to-End Testing</a> - testing, javascript and ruby</p></li><li><p><a href="https://blog.rstankov.com/refactoring-first-vs-change-first/">Refactoring First vs Change First</a> - software engineering</p></li><li><p><a href="https://blog.rstankov.com/my-advice-to-junior-developers/">My Advice to Junior Developers</a> - software engineering</p></li><li><p><a href="https://blog.rstankov.com/testing-graphql-backend-in-product-hunt/">Testing GraphQL Backend in Product Hunt</a> - graphql, ruby and testing</p></li><li><p><a href="https://blog.rstankov.com/dealing-with-n-1-in-graphql-part-2/">Dealing with N+1 in GraphQL</a> (<a href="https://blog.rstankov.com/dealing-with-n-1-with-graphql-part-1/">part 1</a> and <a href="https://blog.rstankov.com/dealing-with-n-1-in-graphql-part-2/">part 2</a>) - graphql and ruby</p></li><li><p><a href="https://blog.rstankov.com/graphql-and-react/">Graphql Optimization Story</a> - story, graphql and react</p></li><li><p><a href="https://blog.rstankov.com/how-i-use-react-context/">How I use React.Context</a> - react</p></li><li><p><a href="https://blog.rstankov.com/extract-react-hook-refactoring/">Extract React Hook Refactoring</a> - react</p></li><li><p><a href="https://blog.rstankov.com/handling-external-service-in-rails/">Organizing External Services in Rails</a> - ruby on rails</p></li><li><p><a href="https://blog.rstankov.com/how-not-implement-html-editor/">How Not Implement Html Editor</a> - story and software engineering</p></li><li><p><a href="https://blog.rstankov.com/bug-duty-process/">Bug Duty Process</a> - process</p></li></ul>]]></content:encoded></item></channel></rss>