<?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[Techwood]]></title><description><![CDATA[Architecture, automation, personal tips and tricks. Everything to make your developer experience in the AI era smooth and enjoyable]]></description><link>https://techwood.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!rnLo!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c3a15b9-1163-4502-a93f-61e876497f87_896x896.png</url><title>Techwood</title><link>https://techwood.substack.com</link></image><generator>Substack</generator><lastBuildDate>Sat, 18 Jul 2026 19:33:06 GMT</lastBuildDate><atom:link href="https://techwood.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Olexandr Pylypyshyn]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[techwood@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[techwood@substack.com]]></itunes:email><itunes:name><![CDATA[Olexandr]]></itunes:name></itunes:owner><itunes:author><![CDATA[Olexandr]]></itunes:author><googleplay:owner><![CDATA[techwood@substack.com]]></googleplay:owner><googleplay:email><![CDATA[techwood@substack.com]]></googleplay:email><googleplay:author><![CDATA[Olexandr]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[I asked successful entrepreneurs about their transition from employee to founder]]></title><description><![CDATA[Learn how successful founders navigated the leap from employee to entrepreneur. Real stories, emotional challenges, and practical lessons for your first startup step.]]></description><link>https://techwood.substack.com/p/successful-entrepreneurs-interview</link><guid isPermaLink="false">https://techwood.substack.com/p/successful-entrepreneurs-interview</guid><dc:creator><![CDATA[Olexandr]]></dc:creator><pubDate>Sun, 30 Nov 2025 14:35:16 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!l1ci!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e301698-10c8-4ad1-a885-ab35834372ef_1400x1400.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!l1ci!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e301698-10c8-4ad1-a885-ab35834372ef_1400x1400.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!l1ci!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e301698-10c8-4ad1-a885-ab35834372ef_1400x1400.png 424w, https://substackcdn.com/image/fetch/$s_!l1ci!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e301698-10c8-4ad1-a885-ab35834372ef_1400x1400.png 848w, https://substackcdn.com/image/fetch/$s_!l1ci!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e301698-10c8-4ad1-a885-ab35834372ef_1400x1400.png 1272w, https://substackcdn.com/image/fetch/$s_!l1ci!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e301698-10c8-4ad1-a885-ab35834372ef_1400x1400.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!l1ci!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e301698-10c8-4ad1-a885-ab35834372ef_1400x1400.png" width="1400" height="1400" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2e301698-10c8-4ad1-a885-ab35834372ef_1400x1400.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1400,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:3147500,&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;:&quot;https://techwood.substack.com/i/180307099?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e301698-10c8-4ad1-a885-ab35834372ef_1400x1400.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!l1ci!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e301698-10c8-4ad1-a885-ab35834372ef_1400x1400.png 424w, https://substackcdn.com/image/fetch/$s_!l1ci!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e301698-10c8-4ad1-a885-ab35834372ef_1400x1400.png 848w, https://substackcdn.com/image/fetch/$s_!l1ci!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e301698-10c8-4ad1-a885-ab35834372ef_1400x1400.png 1272w, https://substackcdn.com/image/fetch/$s_!l1ci!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e301698-10c8-4ad1-a885-ab35834372ef_1400x1400.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>The following is the conversation with&#8230; just kidding &#128516;</p><p>This year, I attended the <a href="https://www.linkedin.com/company/itarena">Lviv IT Arena</a>. I met a few founders there and asked each of them to describe the very beginning of their entrepreneurial journey. If you&#8217;ve ever daydreamed about starting something on your own but didn&#8217;t know where to begin, you&#8217;ll see that even successful founders once stood exactly where you are.</p><p>In this article, you won&#8217;t find polished success stories. This is where you&#8217;ll get to know how the leap from employee to founder truly feels like.</p><p>I don&#8217;t dare to take more of your time, so the first question is&#8230;</p><h2>How did you cope with the switch from employee to founder?</h2><p><em>What did the transition period look like for you?</em></p><p><strong>Anton Avrynskyi,</strong> <a href="https://www.linkedin.com/company/18760739/?lipi=urn%3Ali%3Apage%3Ad_flagship3_profile_view_base_position_details%3B0JLWft%2BpQNuuuzE0W20BUA%3D%3D">CEO &amp; co-founder of Liki24</a> (former Deputy Chief Executive Officer, IT-Enterprise): The first thing you need when moving from employee to business owner is a financial cushion.</p><blockquote><p><em><a href="https://www.robinwaite.com/blog/how-to-become-financially-stable-before-creating-a-startup">You should have enough saved to give yourself at least six months to fully switch gears without constantly worrying about money.</a></em></p></blockquote><p>This buffer won&#8217;t last forever, but it makes the transition manageable.</p><p>The second thing is understanding what truly excites you. I strongly recommend going to events like IT Arena and other startup competitions. Listening to pitches and hearing different founders speak is one of the best ways to discover what resonates with you.</p><p></p><p><strong>Oleksandr Shcherbak,</strong> <a href="https://www.linkedin.com/company/lilo-ua/">CBDO &amp; co-founder of Lilo</a> (former Chief Business Officer, HELSI): Frankly, it was both exciting and terrifying. You constantly wonder whether it will work or not. As an employee, you have stability, and giving that up is never easy. But I started to stagnate after the team of professionals, which I was a part of, made HELSI a major player and a market leader.</p><blockquote><p><em>Everything was running smoothly, but I felt like the race was over.</em></p></blockquote><p>I missed the drive and challenge, and at that point, I realized it was time to build something new.</p><p></p><p><strong>Sergiy Danylov</strong>, <a href="https://www.linkedin.com/company/animahelp/">Co-Founder of Anima</a> (also Professor Assistant, Taras Shevchenko National University of Kyiv): The main challenge wasn&#8217;t the role shift. It was carrying responsibility for the people. You&#8217;re suddenly the one who <a href="https://foundersnetwork.com/startup-fundraising/">has to make sure salaries get paid, and investments are found</a>. This cycle of &#8220;I must, I must, I must&#8221; never really stops.</p><blockquote><p><em>The hardest part is planning how long you can operate in that mode, and asking yourself whether you can bear it again and again.</em></p></blockquote><p></p><p><strong>Ivan Fedorenko,</strong> CEO of a few businesses: When I joined a French company, I felt comfortable, but I knew it wasn&#8217;t enough. Everything was fine, yet I felt pulled toward something different. I had so much energy at the time. I worked eight hours at my job, and in the evenings and on weekends, I was involved in two or three startups at once. Yet, I managed to avoid burnout and rarely felt exhausted.</p><blockquote><p><em>Looking back, I think I rested by switching between different types of work, and that variety actually kept me energized during the transition from employee to founder.</em></p></blockquote><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://techwood.substack.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">Enjoying the conversations so far? Subscribe to receive more posts like this!</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><h2>Did you take a sabbatical or a long break to launch a company?</h2><p><em>What helped you realize it was the right moment to stop and fully pursue your new career?</em></p><p><strong>Anton Avrynskyi</strong>: I believe sabbatical, when you stay employed but &#8220;take a break&#8221;, is a half measure. You&#8217;re still holding a safety net, a plan B that prevents you from going all in. For me, that approach doesn&#8217;t work. <a href="https://www.startupwithferas.com/side-hustle-or-full-time-business/">You need to make a clean break.</a></p><p>That&#8217;s what I did. After 15 years at the same company, first as an employee and later as a partner, I decided to leave completely.</p><blockquote><p><em>To create something new, you have to stop doing the old thing entirely.</em></p></blockquote><p>That was my mindset. I had some savings and sold my share in the company, which gave me a cushion.</p><p></p><p><strong>Oleksandr Shcherbak</strong>: No, while working at HELSI, the same group that later became the founders of Lilo began discussing the idea and concept behind the new product. We held our first strategic sessions and started crystallizing the vision together with our key founder and shareholder, Samvel Akobyan.</p><blockquote><p><em>I knew I had to switch completely once it became clear it would require full-time commitment and investment.</em></p></blockquote><p>The transition took about six months. I worked as Commercial Director at the time, so I needed to find a replacement and hand over my responsibilities. The entire transition was smooth for both HELSI and me, and gave everyone time to prepare. Once I left, I focused on building Lilo right off the bat.</p><p></p><p><strong>Sergiy Danylov:</strong> I didn&#8217;t take a sabbatical.</p><blockquote><p><em>The real turning point came when I realized I couldn&#8217;t split my attention between working, teaching, and building something new any longer. I was simply running out of time.</em></p></blockquote><p>The significant shift occurred when I was moving in a completely new direction. Around 2016, I recognized that the future of AI would rely on understanding human behavior, attention, and cognitive patterns, rather than merely on objects.</p><p>Once I identified this trend and clearly saw that the new field required my full commitment, I stopped juggling multiple roles and dedicated myself entirely to building my own company.</p><p></p><p><strong>Ivan Fedorenko</strong>: I didn&#8217;t take a sabbatical because I felt tied to the company and responsible for delivering results. Only in the third year did I understand what was coming next. First, things finally started working the way I wanted them to. Second, I realized I could achieve more in my own company.</p><blockquote><p><em>That clarity showed me it was time to rethink my path and build something entirely by myself.</em></p></blockquote><h2>Who did you consult with when making the decision?</h2><p><em>Who gave you the most valuable advice? What communities or networks were helpful to you?</em></p><p><strong>Anton Avrynskyi</strong>: I talked to many technical and business-minded people in my network. It&#8217;s important to share your ideas widely with developers, entrepreneurs, investors, and anyone who can challenge your thinking. I reconnected with old contacts and reached out to friends and family. I also met new people via introductions and steadily grew my talking circle.</p><p>Once I had a rough idea of what I wanted to build, I started meeting people involved in that space for feedback. You need these conversations because they help you see blind spots. Some people will tell you your idea is bad, and that&#8217;s fine. The key is not to listen to everyone, but to collect different perspectives and form a clear picture of what you want to build.</p><p>Once you find the idea that excites you the most, move quickly.</p><blockquote><p><em>The discussion phase shouldn&#8217;t last more than a month.</em></p></blockquote><p>I know many people who spend years exploring different notions without ever starting. As a result, their ideas just get outdated.</p><p></p><p><strong>Oleksandr Shcherbak</strong>: I can&#8217;t name one single person who gave the most valuable advice. I gained many useful insights from different people and perspectives.</p><blockquote><p><em>My network was crucial to my progress.</em></p></blockquote><p>I relied on it a lot. My team and I ran informal surveys among our friends and acquaintances, asking questions like:</p><ul><li><p>&#8220;What would an ideal insurance look like for you?&#8221;</p></li><li><p>&#8220;What pain points do you currently experience in healthcare?&#8221;</p></li></ul><p>Those conversations helped us understand better the real user needs and shape our idea accordingly.</p><p></p><p><strong>Sergiy Danylov:</strong> When people interview me about psychology and ask for advice, I always say the same thing:</p><blockquote><p><em>The first rule is not to blindly follow anyone&#8217;s advice.</em></p></blockquote><p>This doesn&#8217;t mean you should isolate yourself. You need to listen widely, stay in the field, absorb as many signals, ideas, and stories as possible. We are imitators by design. That&#8217;s how humans learn. The value comes from recombining what you see around you into something that fits your own context. So the most valuable &#8220;advice&#8221; is not a specific sentence someone told me.</p><p>At the same time, I am genuinely grateful to many people whose wisdom and experience have shaped how I think.</p><p></p><p><strong>Ivan Fedorenko</strong>: I spoke with a friend who was one year ahead of me and had already founded his own IT company. He gave me top-notch advice:</p><blockquote><p><em>Launch the first product version as an MVP (Minimum Viable Product) and don&#8217;t pour all your money into it.</em></p></blockquote><p>I didn&#8217;t listen, and that was a mistake.</p><p>Later, I began studying in business schools, taking management courses, and joining accelerator programs. Those programs were especially valuable because of the networking. You meet people who fall into the same traps, and it&#8217;s easy to learn by watching peers face challenges similar to yours. And, of course, the mentors you meet along the way give you the knowledge that helps you move forward.</p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://techwood.substack.com/p/successful-entrepreneurs-interview?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">If you like this piece, take a moment to share the post with those who can find it useful</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://techwood.substack.com/p/successful-entrepreneurs-interview?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://techwood.substack.com/p/successful-entrepreneurs-interview?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><h2>How did you manage your personal priorities?</h2><p><em>Health, relationships, and personal life. What compromises did you have to make?</em></p><p><strong>Anton Avrynskyi</strong>: In the first years of building my own company, I didn&#8217;t really have a life outside of work. I was obsessed with it, talking about it at home, living it every day. At that time, I already had two small kids. I tried to be present, but the reality was that 90% of my time and energy went into the business.</p><p>I don&#8217;t believe it&#8217;s possible to build something from scratch without being fully dedicated to it, at least, not in my experience. I was hands-on, creating everything from zero, and that required total focus. There was no balance in those first two years. It was roughly 90% work and 10% family, and you have to be mentally prepared for that.</p><blockquote><p><em>The idea of balance simply doesn&#8217;t exist during the early stages.</em></p></blockquote><p></p><p><strong>Oleksandr Shcherbak</strong>: I used to work in a mode that doesn&#8217;t fit into the standard &#8220;9 to 5&#8221; format. I often have to adjust plans on the fly, work at a high pace, or postpone personal matters for the sake of urgent tasks. In that rhythm, launching Lilo wasn&#8217;t a dramatic shift, but it definitely increased the intensity.</p><p>The hardest part is maintaining balance between work and the time I want to dedicate to the people who matter to me - family, my close circle, and friends. The reality of building a startup means having less time to rest. But I consciously try to stay connected, pay attention to my personal life, and not lose the sense of life beyond work.</p><p>For me, it&#8217;s crucial to do something that genuinely inspires and energizes me.</p><blockquote><p><em>When you love what you do, you have enough emotional resources even in a high-intense pace, and the rest you need is mostly physical, just to let your body keep up with the tempo your mind sets.</em></p></blockquote><p>That&#8217;s what helps avoid burnout and stay productive without losing the support of the people closest to you.</p><p></p><p><strong>Sergiy Danylov:</strong> Speaking as a neuroscientist rather than a founder, my general recommendation is to twist your life into a &#8220;rope of habits&#8221; that consistently pulls you toward your goals. And that rope must include plenty of rest, even though that&#8217;s especially hard for a founder.</p><p>You also need some physical activity. Unfortunately, startup life often means sitting all day, which isn&#8217;t healthy. At the same time, the very sense of purpose behind what you&#8217;re building provides enormous psychological protection.</p><blockquote><p><em><a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC9807176/">There&#8217;s nothing more powerful for mental health than having a clear goal and meaning.</a></em></p></blockquote><p></p><p><strong>Ivan Fedorenko</strong>:</p><blockquote><p><em>For me, startups and the process of founding companies became my main life.</em></p></blockquote><p>Many of my friends were connected to the company, so relationships and work were intertwined.</p><p>I didn&#8217;t feel any discomfort at that time, but I often had to work nights and miss sleep, which definitely hurt my health.</p><h2>How and when did you tell your colleagues about your decision to leave and start a company?</h2><p><em>What emotional challenges came up, and how did you handle them?</em></p><p><strong>Anton Avrynskyi</strong>: It was an incredibly difficult period, probably one of the hardest in my career. I had been thinking about leaving for several years before I finally decided to go. There were long months of preparation and reflection. Emotionally, it was devastating.</p><p>When I finally decided, I just had an honest conversation with the two founders. Of course, they were shocked, but we agreed I would stay for six more months to ensure a smooth transition. During that period, I worked at full capacity, covered various areas, trained colleagues, and transferred all my responsibilities.</p><p>A month before leaving, I shared the news with the rest of the team. They were also shocked, but I believe transparency and preparation are the key. You can&#8217;t just announce one day that you&#8217;re leaving tomorrow. It has to be handled carefully and respectfully.</p><p>Looking back, I&#8217;m proud of how it went. We&#8217;re still on great terms with the founders and the team. We stay in touch, help each other whenever possible, and remain friends.</p><blockquote><p><em>My philosophy is simple: come in and leave with integrity.</em></p></blockquote><p></p><p><strong>Oleksandr Shcherbak</strong>: I spoke directly with the CEO of HELSI. Since I was an effective manager during my time at HELSI, we repeatedly tried to launch initiatives aimed at expanding cooperation with insurance companies, so the conversation wasn&#8217;t difficult. We had always had a good relationship, so I wanted my departure to be as smooth as possible for the company.</p><blockquote><p><em>I approached it responsibly: trained the team, shared new ideas, and transferred all the necessary knowledge before leaving</em></p></blockquote><p>Another important factor was that I had the best team under my leadership - mature, independent managers. By the time I left, I was no longer a &#8220;babysitter&#8221; for them, so the transition felt natural and comfortable.</p><p>This approach ensured a smooth process and helped avoid any disruptions for HELSI.</p><p></p><p><strong>Sergiy Danylov:</strong> It was genuinely difficult.</p><blockquote><p><em>After working for twenty years in the same place, you reach a moment when you simply stand up and walk away. That shift hits you hard.</em></p></blockquote><p>We had an outstanding department head. He was disappointed that my partner and I were leaving, but he understood that we were stepping toward something we truly wanted. That understanding made the transition emotional but ultimately honest and respectful for everyone involved.</p><p></p><p><strong>Ivan Fedorenko</strong>: I told my colleagues I wasn&#8217;t abandoning them. I assured them they could reach out to me anytime for questions or support.</p><blockquote><p><em>I didn&#8217;t close the doors.</em></p></blockquote><p>I thanked them, said I genuinely enjoyed working with them, and explained that I needed to move forward for the sake of my own path. There were no negative emotions. We ended things on an optimistic note.</p><h2>Conclusion</h2><p>In this article, I don&#8217;t want to provide the summary and disrupt your thoughts. I hope you will draw your own conclusions based on these conversations.</p><p>My personal takeaway can be described by this quote:</p><blockquote><p><em>&#8220;And sacrifice scares you, but sacrifice brings growth and growth is exactly what you need. Define your priorities, forget your balance&#8221;</em></p></blockquote><h3>Continue reading</h3><p>If these reflections made you curious about the practical side of building your first company, you may want to read my <a href="https://techwood.substack.com/p/10-mistakes-i-made-as-a-first-time">10 mistakes I made as a first-time solo founder</a>.</p><div><hr></div><p>As usual, thank you for your time. I would appreciate it if you hit that like button, and I look forward to hearing your thoughts in the comments!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://techwood.substack.com/p/successful-entrepreneurs-interview/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://techwood.substack.com/p/successful-entrepreneurs-interview/comments"><span>Leave a comment</span></a></p><p>&#128279; <a href="https://www.linkedin.com/in/olexandr-pylypyshyn/">Follow me on LinkedIn</a></p><p>&#128240; Read me on the platform of your choice: <a href="https://techwood.substack.com/">Substack</a>, <a href="https://dev.to/techwood">DEV.to</a></p><p>Till next time! &#128075;</p>]]></content:encoded></item><item><title><![CDATA[10 mistakes I made as a first-time solo founder]]></title><description><![CDATA[Learn 10 proven lessons from a solo engineer who turned founder. Avoid costly startup mistakes and build smarter from day one.]]></description><link>https://techwood.substack.com/p/10-mistakes-i-made-as-a-first-time</link><guid isPermaLink="false">https://techwood.substack.com/p/10-mistakes-i-made-as-a-first-time</guid><dc:creator><![CDATA[Olexandr]]></dc:creator><pubDate>Tue, 21 Oct 2025 21:56:14 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/5b4a1459-e1f0-45dc-b58d-cc41419e7440_1920x1280.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Once in my life, I tried to step down from the engineering role and kick off my own startup. The outcome? Well, I failed - the harsh reality of <a href="https://explodingtopics.com/blog/startup-failure-stats">90% of all startups</a>.</p><p>Yet, I don&#8217;t regret it, although it was a hard time until I ran out of cash and realized the idea wasn&#8217;t as groundbreaking as it had seemed at the beginning. My price of one year building the company (while working full-time) included burnout, mental health problems, constant lack of energy, relationship issues, and an almost-lost job.</p><p>Still, I&#8217;m grateful for all the business, management, and life lessons I&#8217;ve learned during this period. Looking back now, I understand it&#8217;s perhaps the only way to learn these lessons.</p><blockquote><p><strong>NOTE:</strong> If you&#8217;re curios, my startup was called Imabulary. The idea was simple: you take a photo, the app detects the object in the photo, and generates a flashcard in your native and target languages. Then you could organize these flashcards into sets and take different quizzes on these sets. Find more in <a href="https://docs.google.com/presentation/d/1EBG61JjiDqw1VkwLloq2L4KnyD2iNnNH1uMio3VHVbg/edit?usp=sharing">our pitch deck.</a></p></blockquote><p>In this article, I want to reflect on 10 mistakes I made while building a startup. My goal is to help you avoid them and accelerate your certainly difficult but exciting journey. I&#8217;m going to share the mistakes in the form of an actionable checklist. I will state the error, explain why it&#8217;s an error, and give some recommendations, how I&#8217;d do it if I had to start over.</p><blockquote><p><strong>NOTE:</strong> Before we start, note that the mistakes I cover mainly apply to entrepreneurship, but there&#8217;s another form of building a business called solopreneurship. You can <a href="https://www.uschamber.com/co/start/startup/what-is-solopreneur">read more about solopreneurship in this article</a> to see if it better matches your personal goals and values. If it does, not all the points from the article might be useful for you, but I&#8217;m sure, you&#8217;ll still be able to find a valuable information.</p></blockquote><p>With that said, let&#8217;s dive straight into the first mistake.</p><h2>1. Didn&#8217;t find a co-founder</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QiyE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55669fb2-8178-4a1f-82dd-80886650faa3_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QiyE!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55669fb2-8178-4a1f-82dd-80886650faa3_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!QiyE!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55669fb2-8178-4a1f-82dd-80886650faa3_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!QiyE!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55669fb2-8178-4a1f-82dd-80886650faa3_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!QiyE!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55669fb2-8178-4a1f-82dd-80886650faa3_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QiyE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55669fb2-8178-4a1f-82dd-80886650faa3_1024x1024.png" width="422" height="422" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/55669fb2-8178-4a1f-82dd-80886650faa3_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:422,&quot;bytes&quot;:1971292,&quot;alt&quot;:&quot;Two professionals shaking hands, symbolizing partnership and business collaboration.&quot;,&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;:&quot;https://techwood.substack.com/i/176746777?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55669fb2-8178-4a1f-82dd-80886650faa3_1024x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Two professionals shaking hands, symbolizing partnership and business collaboration." title="Two professionals shaking hands, symbolizing partnership and business collaboration." srcset="https://substackcdn.com/image/fetch/$s_!QiyE!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55669fb2-8178-4a1f-82dd-80886650faa3_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!QiyE!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55669fb2-8178-4a1f-82dd-80886650faa3_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!QiyE!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55669fb2-8178-4a1f-82dd-80886650faa3_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!QiyE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55669fb2-8178-4a1f-82dd-80886650faa3_1024x1024.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><h3>Why it&#8217;s a problem</h3><p>Managing every part of a new startup alone is exhausting.</p><p>Here&#8217;s a tiny list of things you&#8217;ll have to deal with from day one:</p><ul><li><p>Tons of customer interviews per day</p></li><li><p>Pitching</p></li><li><p>Participating in incubators and accelerators</p></li><li><p>Writing code</p></li><li><p>Talking to partners and investors</p></li><li><p>Selling</p></li></ul><p>Another important aspect is that most <a href="https://chrisneumann.com/archives/why-do-solo-founders-make-vcs-nervous">VCs prefer teams over solo founders</a> since co-founders can diversify risks. They reasonably see solo-founded companies as more likely to fail and as those that can be led in a dictatorial or narrow-minded way.</p><h3>What to do</h3><p>The answer is simple! Find a partner or two.</p><p>Ideally, your business partner is your friend, classmate, or at least an acquaintance. It&#8217;s also important that you both share the same vision and have strong willpower to build a company. But watch out! Sometimes, things can become so tough that they <a href="https://en.fuckupnights.com/read/friendship-business-failure">can ruin even a close friendship</a>. I&#8217;m not saying this situation is 100% unavoidable, but be aware of such a scenario.</p><blockquote><p><strong>NOTE:</strong> But what if I don&#8217;t have the right person to be my co-founder at the moment? Don&#8217;t worry, Y Combinator (one of the largest startup accelerators in the world) created a <a href="https://www.ycombinator.com/cofounder-matching">Co&#8209;Founder Matching Platform</a>. This is the place where you can find a business partner even if you don&#8217;t have a business idea yet.</p></blockquote><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://techwood.substack.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 Techwood! 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><h2>2. Became a CEO instead of CTO</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!u2kH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534aeec2-ed52-45f2-8bec-d5772d0e795b_1024x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!u2kH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534aeec2-ed52-45f2-8bec-d5772d0e795b_1024x1536.png 424w, https://substackcdn.com/image/fetch/$s_!u2kH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534aeec2-ed52-45f2-8bec-d5772d0e795b_1024x1536.png 848w, https://substackcdn.com/image/fetch/$s_!u2kH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534aeec2-ed52-45f2-8bec-d5772d0e795b_1024x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!u2kH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534aeec2-ed52-45f2-8bec-d5772d0e795b_1024x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!u2kH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534aeec2-ed52-45f2-8bec-d5772d0e795b_1024x1536.png" width="300" height="450" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/534aeec2-ed52-45f2-8bec-d5772d0e795b_1024x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1536,&quot;width&quot;:1024,&quot;resizeWidth&quot;:300,&quot;bytes&quot;:3026695,&quot;alt&quot;:&quot;Businessman inside a fishbowl working on a laptop, symbolizing isolation and work pressure.&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;:&quot;https://techwood.substack.com/i/176746777?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534aeec2-ed52-45f2-8bec-d5772d0e795b_1024x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Businessman inside a fishbowl working on a laptop, symbolizing isolation and work pressure." title="Businessman inside a fishbowl working on a laptop, symbolizing isolation and work pressure." srcset="https://substackcdn.com/image/fetch/$s_!u2kH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534aeec2-ed52-45f2-8bec-d5772d0e795b_1024x1536.png 424w, https://substackcdn.com/image/fetch/$s_!u2kH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534aeec2-ed52-45f2-8bec-d5772d0e795b_1024x1536.png 848w, https://substackcdn.com/image/fetch/$s_!u2kH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534aeec2-ed52-45f2-8bec-d5772d0e795b_1024x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!u2kH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F534aeec2-ed52-45f2-8bec-d5772d0e795b_1024x1536.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>Why it&#8217;s a problem</h3><p>This one comes from the first point. Because I didn&#8217;t have a co-founder, I had to become a CEO (Chief <em>Everything</em> Officer).</p><p>If you&#8217;re passionate about technologies like me, you are going to feel like a fish out of water.</p><h3>What to do</h3><p>Same solution: find a co-founder, preferably a non-technical one (such as a product manager or marketer). Once you have a well-matched partner(s), choose a roles that best match your expertise, so everyone&#8217;s doing what they truly enjoy.</p><p>Watch out! Finding a co-founder(s) doesn&#8217;t mean you should stay isolated in your world of algorithms and patterns, avoiding real communication. It must be exactly the opposite. You have to work closely with your business partners and customers to ensure that you don&#8217;t build something that no one needs. Still, being a CTO lets you focus on technology more than on other aspects of the company.</p><h2>3. Extended the team too early</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!gLkf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F937c2e8d-7951-44f1-a6b1-e272dc042f31_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!gLkf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F937c2e8d-7951-44f1-a6b1-e272dc042f31_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!gLkf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F937c2e8d-7951-44f1-a6b1-e272dc042f31_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!gLkf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F937c2e8d-7951-44f1-a6b1-e272dc042f31_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!gLkf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F937c2e8d-7951-44f1-a6b1-e272dc042f31_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!gLkf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F937c2e8d-7951-44f1-a6b1-e272dc042f31_1536x1024.png" width="496" height="330.7802197802198" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/937c2e8d-7951-44f1-a6b1-e272dc042f31_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:496,&quot;bytes&quot;:2744232,&quot;alt&quot;:&quot;Seated employee smiling as three colleagues stand nearby, suggesting team welcome or onboarding.&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;:&quot;https://techwood.substack.com/i/176746777?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F937c2e8d-7951-44f1-a6b1-e272dc042f31_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Seated employee smiling as three colleagues stand nearby, suggesting team welcome or onboarding." title="Seated employee smiling as three colleagues stand nearby, suggesting team welcome or onboarding." srcset="https://substackcdn.com/image/fetch/$s_!gLkf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F937c2e8d-7951-44f1-a6b1-e272dc042f31_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!gLkf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F937c2e8d-7951-44f1-a6b1-e272dc042f31_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!gLkf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F937c2e8d-7951-44f1-a6b1-e272dc042f31_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!gLkf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F937c2e8d-7951-44f1-a6b1-e272dc042f31_1536x1024.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>Why it&#8217;s a problem</h3><p>When you hire a skilled engineer, manager, marketer, or whoever else, remember: they came to do THEIR job. Don&#8217;t expect them to build YOUR business. Of course, they can give some advice or opinion in certain moments, but in 99% of the time, building the company is on you and your co-founder(s).</p><p>The people you hire obviously expect something in return. Even if you hire them for &#8220;equity&#8221;, in startup terms, that usually means for free, with a subtle hope that this equity will be worth something one day.</p><h3>What to do</h3><p><a href="https://verbasubtitles.com/">Verba&#8217;s</a> founder put it well: don&#8217;t hire a full-time team <a href="https://www.linkedin.com/feed/update/urn:li:activity:7325900171173650432?updateEntityUrn=urn%3Ali%3Afs_updateV2%3A%28urn%3Ali%3Aactivity%3A7325900171173650432%2CFEED_DETAIL%2CEMPTY%2CDEFAULT%2Cfalse%29">unless you make $1M ARR</a> (Annual Recurring Revenue). Until then, try to cover everything with your co-founders or use part-time freelancers if it&#8217;s faster or cheaper than if you start delving into the topic by yourself.</p><h2>4. Scrum is your enemy in the early stages</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6DKu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F908cc750-ac1a-4429-a4bf-84b4287d0f99_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6DKu!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F908cc750-ac1a-4429-a4bf-84b4287d0f99_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!6DKu!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F908cc750-ac1a-4429-a4bf-84b4287d0f99_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!6DKu!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F908cc750-ac1a-4429-a4bf-84b4287d0f99_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!6DKu!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F908cc750-ac1a-4429-a4bf-84b4287d0f99_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6DKu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F908cc750-ac1a-4429-a4bf-84b4287d0f99_1024x1024.png" width="408" height="408" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/908cc750-ac1a-4429-a4bf-84b4287d0f99_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:408,&quot;bytes&quot;:1930842,&quot;alt&quot;:&quot;Stressed man at laptop surrounded by charts, to-do list, and deadline clock.&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;:&quot;https://techwood.substack.com/i/176746777?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F908cc750-ac1a-4429-a4bf-84b4287d0f99_1024x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Stressed man at laptop surrounded by charts, to-do list, and deadline clock." title="Stressed man at laptop surrounded by charts, to-do list, and deadline clock." srcset="https://substackcdn.com/image/fetch/$s_!6DKu!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F908cc750-ac1a-4429-a4bf-84b4287d0f99_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!6DKu!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F908cc750-ac1a-4429-a4bf-84b4287d0f99_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!6DKu!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F908cc750-ac1a-4429-a4bf-84b4287d0f99_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!6DKu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F908cc750-ac1a-4429-a4bf-84b4287d0f99_1024x1024.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>Why it&#8217;s a problem</h3><p>First of all, I&#8217;m not a hater of Scrum. In fact, I use this framework on every project I work on. Scrum works perfectly in companies that already have significant traction: steady revenue, clients, a growing team, investments, and so on. Meanwhile, from my experience, even lightweight Scrum slows early-stage startups without traction more than it helps. Sprint velocity, backlog grooming, and ceremonies introduce overhead that kills iteration speed long before you achieve product-market fit.</p><p>I understand that experienced Scrum masters and project managers may disagree, but your goal is to ship the MVP and sell it as quickly as possible, not to deal with all the Scrum hassles. At this stage, you&#8217;ll waste your (and worse - your teammates&#8217;) invaluable time, rather than gain real benefits from Scrum.</p><h3>What to do</h3><p>It&#8217;s enough to have 2-3 stand-ups per week with your team so everyone is in the loop. Avoid implementing Scrum until you have clear traction and a full-time team. Try to minimize all the calls, communicate via text, and document everything so you can quickly send a link instead of hopping on a call every time.</p><h2>5. Customer development is a 1-to-1 interview, not a Google Form</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Wb6A!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16bc41b7-c98d-4bd7-b821-c12387875563_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Wb6A!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16bc41b7-c98d-4bd7-b821-c12387875563_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!Wb6A!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16bc41b7-c98d-4bd7-b821-c12387875563_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!Wb6A!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16bc41b7-c98d-4bd7-b821-c12387875563_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!Wb6A!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16bc41b7-c98d-4bd7-b821-c12387875563_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Wb6A!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16bc41b7-c98d-4bd7-b821-c12387875563_1536x1024.png" width="502" height="334.7815934065934" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/16bc41b7-c98d-4bd7-b821-c12387875563_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:502,&quot;bytes&quot;:2760134,&quot;alt&quot;:&quot;Two coworkers discuss ideas while another person scrolls on phone beside a crossed-out monitor.&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;:&quot;https://techwood.substack.com/i/176746777?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16bc41b7-c98d-4bd7-b821-c12387875563_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Two coworkers discuss ideas while another person scrolls on phone beside a crossed-out monitor." title="Two coworkers discuss ideas while another person scrolls on phone beside a crossed-out monitor." srcset="https://substackcdn.com/image/fetch/$s_!Wb6A!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16bc41b7-c98d-4bd7-b821-c12387875563_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!Wb6A!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16bc41b7-c98d-4bd7-b821-c12387875563_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!Wb6A!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16bc41b7-c98d-4bd7-b821-c12387875563_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!Wb6A!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16bc41b7-c98d-4bd7-b821-c12387875563_1536x1024.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>What is customer development? It&#8217;s the process of finding who our customers really are and which problems they need solved. Think of it as debugging your business idea, and interviews are your breakpoints.</p><h3>Why it&#8217;s a problem</h3><ol><li><p><strong>Diverse and unreliable audience -</strong> in the chats, where you probably share your customer development form, there are people with very different interests and experiences. They can be engineers, managers, or just strangers. Most of the time, people who fill out the form won&#8217;t belong to your target audience.</p></li><li><p><strong>Superficial answers -</strong> filling the form is a formal, routine process. The majority of people won&#8217;t dive into the details, so the quality of the data you receive is questionable.</p></li><li><p><strong>Missing context -</strong> you cannot understand the context in which the respondent is answering. You don&#8217;t see what issues really concern the user and whether their answer corresponds to the real situation.</p></li></ol><h3>What to do</h3><p>Customer development is a critical topic, especially in early-stage startups. Unfortunately, it&#8217;s impossible to tell you everything in one article. My top three recommendations are:</p><ol><li><p>Conduct 1-to-1 interviews with people you know who belong to your target audience. It will help you to avoid the problems discussed above.</p></li><li><p>Read <a href="https://www.amazon.com/Mom-Test-customers-business-everyone/dp/1492180742">&#8220;The Mom Test&#8221;</a> and <a href="https://www.amazon.com/-/en/Steve-Blank/dp/0989200507">&#8220;The Four Steps to the Epiphany&#8221;</a>. They&#8217;re often referenced as the Bibles of customer development.</p></li><li><p>Join <a href="https://www.eo.in.ua/">eo Incubator</a>. They provide three amazing workshops that teach you customer development from zero to hero. I&#8217;ve conducted them too, and it&#8217;s not an ad, but personal, genuine advice.</p></li></ol><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://techwood.substack.com/p/10-mistakes-i-made-as-a-first-time?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Enjoying the article so far? Help others love it too by spreading the word about it</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://techwood.substack.com/p/10-mistakes-i-made-as-a-first-time?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://techwood.substack.com/p/10-mistakes-i-made-as-a-first-time?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><h2>6. Conducted pre-sale too late</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_oDx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8458c877-0bb4-465c-8c7b-e8a68420557a_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_oDx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8458c877-0bb4-465c-8c7b-e8a68420557a_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!_oDx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8458c877-0bb4-465c-8c7b-e8a68420557a_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!_oDx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8458c877-0bb4-465c-8c7b-e8a68420557a_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!_oDx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8458c877-0bb4-465c-8c7b-e8a68420557a_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_oDx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8458c877-0bb4-465c-8c7b-e8a68420557a_1536x1024.png" width="478" height="318.7760989010989" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8458c877-0bb4-465c-8c7b-e8a68420557a_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:478,&quot;bytes&quot;:3014116,&quot;alt&quot;:&quot;Marketer working on laptop with loudspeaker and gift-icon screen showing sign-up offer.&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;:&quot;https://techwood.substack.com/i/176746777?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8458c877-0bb4-465c-8c7b-e8a68420557a_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Marketer working on laptop with loudspeaker and gift-icon screen showing sign-up offer." title="Marketer working on laptop with loudspeaker and gift-icon screen showing sign-up offer." srcset="https://substackcdn.com/image/fetch/$s_!_oDx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8458c877-0bb4-465c-8c7b-e8a68420557a_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!_oDx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8458c877-0bb4-465c-8c7b-e8a68420557a_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!_oDx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8458c877-0bb4-465c-8c7b-e8a68420557a_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!_oDx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8458c877-0bb4-465c-8c7b-e8a68420557a_1536x1024.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>First of all, <strong>pre-sale != B2B deals!</strong></p><p>In my case, &#8220;pre-sale&#8221; is the stage when we don&#8217;t have a product yet, not even an MVP, but we already try to sell it using targeted ads, landing pages, and Figma mockups. Basically, we say to leads, &#8220;Leave us your email, be among the first, collect loyalty bonuses, and help us understand the demand&#8221;.</p><h3>Why it&#8217;s a problem</h3><p>If you start pre-sale too late, you risk missing key information about demand, your audience, and your value proposition. You can still create a product that doesn&#8217;t meet the market needs, even though you have the information from customer development interviews. There&#8217;s no reason to limit yourself to one source if you can do both. This mix lets you create a more targeted product with a sharper value proposition that solves a narrower problem.</p><h3>What to do</h3><p>Conduct pre-sale after the first 15 customer development interviews, <em>not</em> when you&#8217;ve almost finished developing the MVP.</p><blockquote><p><strong>NOTE:</strong> The number of conducted interviews may vary and depends on how well you understand your target audience and their pain points.</p></blockquote><p>Don&#8217;t be afraid that users who signed up on your landing page will forget about it or won&#8217;t use the product. Most of them wouldn&#8217;t be your customers anyway, especially paying ones.</p><p>Also, pre-sale accurately shows whether a real demand exists for your product and if you should move forward or pivot.</p><p>Don&#8217;t just take my word, here&#8217;s <a href="https://buffer.com/resources/idea-to-paying-customers-in-7-weeks-how-we-did-it/">how Buffer did pre-sale in 2011</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_!jbrU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeeba6cc-481a-4b2d-8fd9-5547aa101d43_1824x830.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jbrU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeeba6cc-481a-4b2d-8fd9-5547aa101d43_1824x830.png 424w, https://substackcdn.com/image/fetch/$s_!jbrU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeeba6cc-481a-4b2d-8fd9-5547aa101d43_1824x830.png 848w, https://substackcdn.com/image/fetch/$s_!jbrU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeeba6cc-481a-4b2d-8fd9-5547aa101d43_1824x830.png 1272w, https://substackcdn.com/image/fetch/$s_!jbrU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeeba6cc-481a-4b2d-8fd9-5547aa101d43_1824x830.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jbrU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeeba6cc-481a-4b2d-8fd9-5547aa101d43_1824x830.png" width="1456" height="663" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/eeeba6cc-481a-4b2d-8fd9-5547aa101d43_1824x830.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:663,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:120922,&quot;alt&quot;:&quot;Five-stage startup roadmap: Idea, Customer interviews, Presale, MVP, Scale.&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;:&quot;https://techwood.substack.com/i/176746777?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeeba6cc-481a-4b2d-8fd9-5547aa101d43_1824x830.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Five-stage startup roadmap: Idea, Customer interviews, Presale, MVP, Scale." title="Five-stage startup roadmap: Idea, Customer interviews, Presale, MVP, Scale." srcset="https://substackcdn.com/image/fetch/$s_!jbrU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeeba6cc-481a-4b2d-8fd9-5547aa101d43_1824x830.png 424w, https://substackcdn.com/image/fetch/$s_!jbrU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeeba6cc-481a-4b2d-8fd9-5547aa101d43_1824x830.png 848w, https://substackcdn.com/image/fetch/$s_!jbrU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeeba6cc-481a-4b2d-8fd9-5547aa101d43_1824x830.png 1272w, https://substackcdn.com/image/fetch/$s_!jbrU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeeba6cc-481a-4b2d-8fd9-5547aa101d43_1824x830.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>7. Overbuilt the MVP</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pmAC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f089647-1428-4135-ba24-cafc0aa363ad_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pmAC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f089647-1428-4135-ba24-cafc0aa363ad_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!pmAC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f089647-1428-4135-ba24-cafc0aa363ad_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!pmAC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f089647-1428-4135-ba24-cafc0aa363ad_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!pmAC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f089647-1428-4135-ba24-cafc0aa363ad_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pmAC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f089647-1428-4135-ba24-cafc0aa363ad_1024x1024.png" width="406" height="406" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6f089647-1428-4135-ba24-cafc0aa363ad_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:406,&quot;bytes&quot;:1893957,&quot;alt&quot;:&quot;Developer holding wrench and coding on laptop amid rocket, error sign, and MVP screen.&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;:&quot;https://techwood.substack.com/i/176746777?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f089647-1428-4135-ba24-cafc0aa363ad_1024x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Developer holding wrench and coding on laptop amid rocket, error sign, and MVP screen." title="Developer holding wrench and coding on laptop amid rocket, error sign, and MVP screen." srcset="https://substackcdn.com/image/fetch/$s_!pmAC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f089647-1428-4135-ba24-cafc0aa363ad_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!pmAC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f089647-1428-4135-ba24-cafc0aa363ad_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!pmAC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f089647-1428-4135-ba24-cafc0aa363ad_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!pmAC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f089647-1428-4135-ba24-cafc0aa363ad_1024x1024.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>Why it&#8217;s a problem</h3><p>Experienced founders would say, &#8220;Hah, classic &#128684;&#128511;&#8221;.</p><p>And indeed, it&#8217;s a common pitfall for many startups and their first-timer founders (especially technical!). At Imabulary, we&#8217;ve spent a couple of weeks perfecting the quiz instead of shipping and selling.</p><p>The truth is - you can&#8217;t be sure anyone will buy your product. The fastest way to test it is to try to sell the MVP right away. But how can you make it if you&#8217;re fixing that stupid bug or adding &#8220;just one more feature&#8221;?</p><h3>What to do</h3><p>If your product doesn&#8217;t solve one precise pain point for your customer, they won&#8217;t buy it, no matter how bug-free or feature-packed it is.</p><p>In most consumer or productivity apps, users forgive early bugs if the product relieves their pain. Believe me, they will wait while you fix everything. Just don&#8217;t make them wait endlessly, of course.</p><p>How do you understand whether the MVP needs some functionality or not? If you say &#8220;Yes&#8221; to the question <em>&#8220;Would the application fulfill its purpose if we remove this feature in the MVP?&#8221;</em>, then you don&#8217;t need this feature.</p><h2>8. Selected the wrong revenue model</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!A3Lp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42eb652b-fbf3-43ba-bdad-8f89d14e3baf_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!A3Lp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42eb652b-fbf3-43ba-bdad-8f89d14e3baf_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!A3Lp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42eb652b-fbf3-43ba-bdad-8f89d14e3baf_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!A3Lp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42eb652b-fbf3-43ba-bdad-8f89d14e3baf_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!A3Lp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42eb652b-fbf3-43ba-bdad-8f89d14e3baf_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!A3Lp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42eb652b-fbf3-43ba-bdad-8f89d14e3baf_1024x1024.png" width="366" height="366" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/42eb652b-fbf3-43ba-bdad-8f89d14e3baf_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:366,&quot;bytes&quot;:2000718,&quot;alt&quot;:&quot;Pensive man surrounded by icons of checkmark, X, question mark, and coin symbol.&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;:&quot;https://techwood.substack.com/i/176746777?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42eb652b-fbf3-43ba-bdad-8f89d14e3baf_1024x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Pensive man surrounded by icons of checkmark, X, question mark, and coin symbol." title="Pensive man surrounded by icons of checkmark, X, question mark, and coin symbol." srcset="https://substackcdn.com/image/fetch/$s_!A3Lp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42eb652b-fbf3-43ba-bdad-8f89d14e3baf_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!A3Lp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42eb652b-fbf3-43ba-bdad-8f89d14e3baf_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!A3Lp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42eb652b-fbf3-43ba-bdad-8f89d14e3baf_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!A3Lp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42eb652b-fbf3-43ba-bdad-8f89d14e3baf_1024x1024.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>Why it&#8217;s a problem</h3><p><a href="https://www.togai.com/blog/saas-recurring-revenue-models/">Subscription rules the SaaS world</a>. It remains the dominant and most investor-friendly model. It&#8217;s also a predictable and understandable model for customers.</p><p>As a first-time founder, you&#8217;re better off not reinventing the revenue model until you have market traction. You may think you&#8217;ve come up with some cool monetization approach no one has guessed to implement before. But usually, such &#8220;innovative&#8221; logic makes it harder to calculate <a href="https://wise.com/us/blog/unit-economics">unit economics</a>, investors reject you, and worst of all, users don&#8217;t pay.</p><h3>What to do</h3><p>Start with a simple, proven subscription revenue model. Then add all the other features like credits, tokens, and so on once product-market fit is stable.</p><p>You can also read <a href="https://www.forentrepreneurs.com/saas-metrics-2/">this classic on subscriptions and unit economics</a> by David Skok.</p><h2>9. Operated from personal accounts</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!A6ks!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4002bd3-952c-485e-9384-1fbe8c68b767_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!A6ks!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4002bd3-952c-485e-9384-1fbe8c68b767_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!A6ks!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4002bd3-952c-485e-9384-1fbe8c68b767_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!A6ks!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4002bd3-952c-485e-9384-1fbe8c68b767_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!A6ks!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4002bd3-952c-485e-9384-1fbe8c68b767_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!A6ks!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4002bd3-952c-485e-9384-1fbe8c68b767_1024x1024.png" width="396" height="396" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b4002bd3-952c-485e-9384-1fbe8c68b767_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:396,&quot;bytes&quot;:2127059,&quot;alt&quot;:&quot;Man thinking before a computer screen with email and X icons, symbolizing communication issues.&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;:&quot;https://techwood.substack.com/i/176746777?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4002bd3-952c-485e-9384-1fbe8c68b767_1024x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Man thinking before a computer screen with email and X icons, symbolizing communication issues." title="Man thinking before a computer screen with email and X icons, symbolizing communication issues." srcset="https://substackcdn.com/image/fetch/$s_!A6ks!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4002bd3-952c-485e-9384-1fbe8c68b767_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!A6ks!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4002bd3-952c-485e-9384-1fbe8c68b767_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!A6ks!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4002bd3-952c-485e-9384-1fbe8c68b767_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!A6ks!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4002bd3-952c-485e-9384-1fbe8c68b767_1024x1024.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>Why it&#8217;s a problem</h3><p>It looks unprofessional when you pitch an investor, ask for $100K and at the end you put &#8220;bigmoneyhustla2002@gmail.com&#8221; as a contact email you created when you were 12. If you can&#8217;t spend $5 on a professional email, then why should an investor trust you with their money?</p><p>Additionally, when you register accounts at Google Cloud Platform, AWS, Play Console, AppStore Connect, and so on, you can put the name of your company instead of your first and last names.</p><p>Last but not least, it&#8217;s more convenient to keep personal and professional lives separate with two different accounts, so emails, calendars, and similar stuff don&#8217;t get mixed.</p><h3>What to do</h3><p>Once customer development interviews confirm the idea is worth pursuing, buy a domain and sign up for a Google Workspace account. Google isn&#8217;t mandatory; you can choose whatever provider you like. I always like to create a &#8220;myname@companyname.com&#8221; email. For example, I used &#8220;olexandr@imabulary.app&#8221; when I was building a startup.</p><h2>10. I wasn&#8217;t the customer of my own product</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!T4rA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4002e11-b431-4f83-85cd-abfb01589716_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!T4rA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4002e11-b431-4f83-85cd-abfb01589716_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!T4rA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4002e11-b431-4f83-85cd-abfb01589716_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!T4rA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4002e11-b431-4f83-85cd-abfb01589716_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!T4rA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4002e11-b431-4f83-85cd-abfb01589716_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!T4rA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4002e11-b431-4f83-85cd-abfb01589716_1024x1024.png" width="388" height="388" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a4002e11-b431-4f83-85cd-abfb01589716_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:388,&quot;bytes&quot;:1759613,&quot;alt&quot;:&quot;Man thinking in front of a computer showing a sad shopping bag icon&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;:&quot;https://techwood.substack.com/i/176746777?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4002e11-b431-4f83-85cd-abfb01589716_1024x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Man thinking in front of a computer showing a sad shopping bag icon" title="Man thinking in front of a computer showing a sad shopping bag icon" srcset="https://substackcdn.com/image/fetch/$s_!T4rA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4002e11-b431-4f83-85cd-abfb01589716_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!T4rA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4002e11-b431-4f83-85cd-abfb01589716_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!T4rA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4002e11-b431-4f83-85cd-abfb01589716_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!T4rA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4002e11-b431-4f83-85cd-abfb01589716_1024x1024.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>Why it&#8217;s a problem</h3><p>If you&#8217;re <em>not</em> tackling a hurdle you&#8217;ve personally faced or are facing now, you won&#8217;t fully understand your client&#8217;s pain. You won&#8217;t be able to create a product that solves a real-world problem for you and your users.</p><p>That&#8217;s why I believe it&#8217;s so ridiculous to ask ChatGPT, &#8220;Generate me 10 ideas for my next 1-billion-dollar startup&#8221;, or to search YouTube for startup ideas.</p><h3>What to do</h3><p>You have to be your first customer. Of course, it doesn&#8217;t mean that you should center the product on your narcissistic vision, because business doesn&#8217;t work like that. No one will buy your product if it&#8217;s only for one person. But, at the same time, it&#8217;s easier to stay motivated and disciplined if you benefit from your own product. Not only in terms of money, but also in terms of the value the product brings to the world.</p><blockquote><p><strong>NOTE:</strong> An yet, if you&#8217;re tackling someone else&#8217;s pain, make sure to embed yourself deeply in the user&#8217;s world - observe, interview, and test relentlessly.</p></blockquote><h2>Conclusion</h2><p>With everything you&#8217;ve just read, can I guarantee that now you&#8217;ll succeed in your founder&#8217;s journey? Nope, unfortunately, I can&#8217;t. No one can. Every startup is unique. You can follow some pieces of advice and avoid common pitfalls and mistakes, but your way will be unrepeatable.</p><p>As I stated in the intro, 9 out of 10 startups fail, so chances are, you&#8217;ll come to nothing as well. But it doesn&#8217;t mean you should just accept this data and stop pursuing your dream. <strong>Be bold.</strong> If an idea keeps you up at night, or you&#8217;ve seen an unresolved problem you believe you can tackle, then do it.</p><p>Remember, &#8220;I&#8217;ve tried and I failed&#8221; is far better than &#8220;I didn&#8217;t try because I was too afraid&#8221;. At the end of the day, who said you can&#8217;t be one out of ten who succeeded?</p><h3>Summary</h3><ol><li><p>Don&#8217;t build alone. Find a co-founder.</p></li><li><p>Do what you know how to do best. Don&#8217;t become Chief Everything Officer.</p></li><li><p>Hire after you have proven traction.</p></li><li><p>Don&#8217;t implement Scrum at the early stages.</p></li><li><p>Talk with customers directly. Don&#8217;t use forms for customer development.</p></li><li><p>Your early stage should look like this: idea &#8594; customer development &#8594; <strong>pre-sale</strong> &#8594; MVP &#8594; scale.</p></li><li><p>Don&#8217;t overbuild your MVP. Always ask yourself, &#8220;Would the application fulfill its purpose if we remove this feature in the MVP?&#8221;.</p></li><li><p>Don&#8217;t reinvent the <s>wheel</s> monetization. Choose a subscription revenue model and build your business model around it.</p></li><li><p>Create a corporate email to manage a company.</p></li><li><p>Try to tackle the real problem you&#8217;ve faced before.</p></li></ol><h3>Want more?</h3><p>Check out these 3 reflective articles, where other startup founders (successful and not) share their experience:</p><ul><li><p><a href="https://www.reddit.com/r/SaaS/comments/1dxvsae/reflections_from_my_first_year_as_a_solo_founder/">Reflections from my first year as a solo founder and entrepreneur</a></p></li><li><p><a href="https://every.to/p/lessons-from-my-failed-startup">Lessons from My Failed Startup</a></p></li><li><p><a href="https://www.taekim.dev/writing/reflections-on-my-first-founder-journey">Reflections on my founder journey</a></p></li></ul><p>And, of course, don&#8217;t forget to give a shot to my previous articles:</p><ul><li><p><a href="https://open.substack.com/pub/techwood/p/three-react-mui-commandments?r=5uvuls&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=false">Three React MUI commandments</a></p></li><li><p><a href="https://open.substack.com/pub/techwood/p/react-mvvm-with-tanstack-router?r=5uvuls&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=false">React MVVM Architecture with TanStack Router: An Introduction</a></p></li></ul><div><hr></div><p>As usual, thank you for your time. I would appreciate it if you hit that like button, and I look forward to hearing your thoughts in the comments!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://techwood.substack.com/p/10-mistakes-i-made-as-a-first-time/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://techwood.substack.com/p/10-mistakes-i-made-as-a-first-time/comments"><span>Leave a comment</span></a></p><p>&#128279; <a href="https://www.linkedin.com/in/olexandr-pylypyshyn/">Follow me on LinkedIn</a></p><p>&#128240; Read me on the platform of your choice: <a href="https://techwood.substack.com/">Substack</a>, <a href="https://dev.to/techwood">DEV.to</a></p><p>Till next time! &#128075;</p>]]></content:encoded></item><item><title><![CDATA[Three React MUI commandments]]></title><description><![CDATA[Learn 3 proven rules for building maintainable MUI React apps. Master theme, styled, and wrapper components to create scalable, clean UI systems.]]></description><link>https://techwood.substack.com/p/three-react-mui-commandments</link><guid isPermaLink="false">https://techwood.substack.com/p/three-react-mui-commandments</guid><dc:creator><![CDATA[Olexandr]]></dc:creator><pubDate>Wed, 17 Sep 2025 10:30:44 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ej8Q!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75052dba-c1db-4cf5-9a3f-5f2690aff232_1494x647.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p></p><p><a href="https://mui.com/material-ui/">MUI (or Material UI)</a> is a popular React library for building feature-rich UI. There are many great competitors, such as <a href="https://ant.design/">Ant Design</a> and <a href="https://ui.shadcn.com/">Shadcn</a>, among others. However, MUI is still my preferred choice when I need to create a robust enterprise frontend application. The library has a <a href="https://mui.com/material-ui/all-components/">rich set of components</a>, <a href="https://mui.com/material-ui/customization/how-to-customize/">flexible theming capabilities</a>, and strong accessibility out of the box.</p><p>Because MUI is powerful, things can often get messy. You&#8217;ve probably experienced this frustration firsthand:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!wdtC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cad11e5-ff38-4489-98a4-fc38e3eae349_1420x1726.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wdtC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cad11e5-ff38-4489-98a4-fc38e3eae349_1420x1726.png 424w, https://substackcdn.com/image/fetch/$s_!wdtC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cad11e5-ff38-4489-98a4-fc38e3eae349_1420x1726.png 848w, https://substackcdn.com/image/fetch/$s_!wdtC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cad11e5-ff38-4489-98a4-fc38e3eae349_1420x1726.png 1272w, https://substackcdn.com/image/fetch/$s_!wdtC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cad11e5-ff38-4489-98a4-fc38e3eae349_1420x1726.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wdtC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cad11e5-ff38-4489-98a4-fc38e3eae349_1420x1726.png" width="1420" height="1726" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9cad11e5-ff38-4489-98a4-fc38e3eae349_1420x1726.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1726,&quot;width&quot;:1420,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2234142,&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;:&quot;https://techwood.substack.com/i/173835028?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cad11e5-ff38-4489-98a4-fc38e3eae349_1420x1726.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!wdtC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cad11e5-ff38-4489-98a4-fc38e3eae349_1420x1726.png 424w, https://substackcdn.com/image/fetch/$s_!wdtC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cad11e5-ff38-4489-98a4-fc38e3eae349_1420x1726.png 848w, https://substackcdn.com/image/fetch/$s_!wdtC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cad11e5-ff38-4489-98a4-fc38e3eae349_1420x1726.png 1272w, https://substackcdn.com/image/fetch/$s_!wdtC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cad11e5-ff38-4489-98a4-fc38e3eae349_1420x1726.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><figcaption class="image-caption"><em>Example of the component we&#8217;ll be refactoring in this article</em></figcaption></figure></div><p>I&#8217;ve been using the library since version 4 on many of my projects. In this article, I share 3 quick tips I learned the hard way. So you can save hours of refactoring and develop more maintainable and testable 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_!ej8Q!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75052dba-c1db-4cf5-9a3f-5f2690aff232_1494x647.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ej8Q!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75052dba-c1db-4cf5-9a3f-5f2690aff232_1494x647.png 424w, https://substackcdn.com/image/fetch/$s_!ej8Q!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75052dba-c1db-4cf5-9a3f-5f2690aff232_1494x647.png 848w, https://substackcdn.com/image/fetch/$s_!ej8Q!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75052dba-c1db-4cf5-9a3f-5f2690aff232_1494x647.png 1272w, https://substackcdn.com/image/fetch/$s_!ej8Q!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75052dba-c1db-4cf5-9a3f-5f2690aff232_1494x647.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ej8Q!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75052dba-c1db-4cf5-9a3f-5f2690aff232_1494x647.png" width="1456" height="631" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/75052dba-c1db-4cf5-9a3f-5f2690aff232_1494x647.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:631,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1110687,&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;:false,&quot;internalRedirect&quot;:&quot;https://techwood.substack.com/i/173835028?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75052dba-c1db-4cf5-9a3f-5f2690aff232_1494x647.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ej8Q!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75052dba-c1db-4cf5-9a3f-5f2690aff232_1494x647.png 424w, https://substackcdn.com/image/fetch/$s_!ej8Q!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75052dba-c1db-4cf5-9a3f-5f2690aff232_1494x647.png 848w, https://substackcdn.com/image/fetch/$s_!ej8Q!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75052dba-c1db-4cf5-9a3f-5f2690aff232_1494x647.png 1272w, https://substackcdn.com/image/fetch/$s_!ej8Q!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75052dba-c1db-4cf5-9a3f-5f2690aff232_1494x647.png 1456w" sizes="100vw"></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><figcaption class="image-caption"><em>Example of refactored component</em></figcaption></figure></div><p>Okay, let&#8217;s get started cleaning up the mess. Here&#8217;s the first rule.</p><h2>Rule number uno - t<strong>heme it or leave it</strong></h2><p>Always start exploring customization possibilities from the <a href="https://mui.com/material-ui/customization/theme-components/">global theme config</a>.</p><p>The global theme config centralizes styles for all MUI components, ensuring consistent styling throughout the entire app. That&#8217;s why, when you make some adjustment to the Material UI component inside the global theme, it will be applied to <em>all </em>the instances of this component across the app.</p><p>Let&#8217;s take a button as an example. MUI&#8217;s default button is in Material Design 2 style:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ayoF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea1ba7b-57f8-43b7-b632-e733cb618282_266x158.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ayoF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea1ba7b-57f8-43b7-b632-e733cb618282_266x158.png 424w, https://substackcdn.com/image/fetch/$s_!ayoF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea1ba7b-57f8-43b7-b632-e733cb618282_266x158.png 848w, https://substackcdn.com/image/fetch/$s_!ayoF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea1ba7b-57f8-43b7-b632-e733cb618282_266x158.png 1272w, https://substackcdn.com/image/fetch/$s_!ayoF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea1ba7b-57f8-43b7-b632-e733cb618282_266x158.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ayoF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea1ba7b-57f8-43b7-b632-e733cb618282_266x158.png" width="266" height="158" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1ea1ba7b-57f8-43b7-b632-e733cb618282_266x158.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:158,&quot;width&quot;:266,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:11386,&quot;alt&quot;:&quot;Blue rectangular button with white text reading &#8220;CONTAINED.&#8221;&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;:&quot;https://techwood.substack.com/i/173835028?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea1ba7b-57f8-43b7-b632-e733cb618282_266x158.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Blue rectangular button with white text reading &#8220;CONTAINED.&#8221;" title="Blue rectangular button with white text reading &#8220;CONTAINED.&#8221;" srcset="https://substackcdn.com/image/fetch/$s_!ayoF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea1ba7b-57f8-43b7-b632-e733cb618282_266x158.png 424w, https://substackcdn.com/image/fetch/$s_!ayoF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea1ba7b-57f8-43b7-b632-e733cb618282_266x158.png 848w, https://substackcdn.com/image/fetch/$s_!ayoF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea1ba7b-57f8-43b7-b632-e733cb618282_266x158.png 1272w, https://substackcdn.com/image/fetch/$s_!ayoF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea1ba7b-57f8-43b7-b632-e733cb618282_266x158.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption"><em>Material Design 2 style of button</em></figcaption></figure></div><p>But what if we want it to match GitHub&#8217;s Primer looks:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!H8O-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcea7b82-5578-43dd-a626-6dba68418244_226x106.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!H8O-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcea7b82-5578-43dd-a626-6dba68418244_226x106.png 424w, https://substackcdn.com/image/fetch/$s_!H8O-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcea7b82-5578-43dd-a626-6dba68418244_226x106.png 848w, https://substackcdn.com/image/fetch/$s_!H8O-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcea7b82-5578-43dd-a626-6dba68418244_226x106.png 1272w, https://substackcdn.com/image/fetch/$s_!H8O-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcea7b82-5578-43dd-a626-6dba68418244_226x106.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!H8O-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcea7b82-5578-43dd-a626-6dba68418244_226x106.png" width="226" height="106" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fcea7b82-5578-43dd-a626-6dba68418244_226x106.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:106,&quot;width&quot;:226,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:8640,&quot;alt&quot;:&quot;Green rectangular button with white text reading &#8220;Contained.&#8221;&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;:&quot;https://techwood.substack.com/i/173835028?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcea7b82-5578-43dd-a626-6dba68418244_226x106.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Green rectangular button with white text reading &#8220;Contained.&#8221;" title="Green rectangular button with white text reading &#8220;Contained.&#8221;" srcset="https://substackcdn.com/image/fetch/$s_!H8O-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcea7b82-5578-43dd-a626-6dba68418244_226x106.png 424w, https://substackcdn.com/image/fetch/$s_!H8O-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcea7b82-5578-43dd-a626-6dba68418244_226x106.png 848w, https://substackcdn.com/image/fetch/$s_!H8O-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcea7b82-5578-43dd-a626-6dba68418244_226x106.png 1272w, https://substackcdn.com/image/fetch/$s_!H8O-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcea7b82-5578-43dd-a626-6dba68418244_226x106.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption"><em>GitHub Primer style of button</em></figcaption></figure></div><p>For that, we create a new theme config and customize the <code>MuiButton</code> component through it:</p><p><a href="https://github.com/ole-techwood/articles/blob/mui/src/theme.ts">theme.tsx</a></p><pre><code>// theme.tsx

import { createTheme } from "@mui/material";

export const theme = createTheme({
  components: {
    // Name of the component
    MuiButton: {
      styleOverrides: {
        root: {
          textTransform: "none",
          boxShadow: "none",
          padding: "0 12px",
          height: 32,
          borderRadius: 6,
          backgroundColor: "rgb(45, 164, 78)",
          border: "1px solid rgba(27, 31, 36, 0.15)",
        },
      },
    },
  },
});</code></pre><blockquote><p><strong>NOTE:</strong> Always use colors from the <a href="https://mui.com/material-ui/customization/palette/">palette</a>. Don&#8217;t put them directly as strings into your theme config. I did to avoid a complex setup and focus strictly on the rule.</p></blockquote><p>Don&#8217;t forget to use <code>ThemeProvider</code> to <a href="https://mui.com/material-ui/customization/theming/#theme-provider">inject a custom theme into your application</a>:</p><p><a href="https://github.com/ole-techwood/articles/blob/mui/src/main.tsx">main.tsx</a></p><pre><code>// main.tsx

//...
createRoot(document.getElementById("root")!).render(
  &lt;StrictMode&gt;
    &lt;CssBaseline /&gt;
    &lt;ThemeProvider theme={theme}&gt;
      &lt;App /&gt;
    &lt;/ThemeProvider&gt;
  &lt;/StrictMode&gt;
);</code></pre><p>But what if the theme doesn&#8217;t give you enough flexibility? That&#8217;s where our second rule comes in.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://techwood.substack.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 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><h2>Rule number two - styled component is what you do</h2><p>Did you carefully investigate <a href="https://mui.com/material-ui/customization/theming/#theme-configuration-variables">all the design tokens and props the theme provides</a> and couldn&#8217;t find the desired one for the component you want to customize? It&#8217;s time to take the next step - create a styled component.</p><p>Let&#8217;s proceed with the button. Assume we want to have 2 shapes: <code>round</code> and <code>square</code>. By default, MUI doesn&#8217;t provide us <code>shape</code> property, but we can easily add it by creating a new styled component:</p><p><a href="https://github.com/ole-techwood/articles/blob/mui/src/button.styled.ts">button.styled.ts</a></p><pre><code>// button.styled.ts

import {
  Button as MuiButton,
  styled,
  type ButtonProps as MuiButtonProps,
} from "@mui/material";

// Determines whether a given prop should be forwarded to the underlying DOM element.
const shouldForwardProp = (prop: string) =&gt; {
  if (!prop || typeof prop !== "string") return false;

  return !prop.trim().startsWith("$");
};

type ButtonProps = MuiButtonProps &amp; {
  $shape?: "round" | "square";
};

export const Button = styled(MuiButton, { shouldForwardProp })&lt;ButtonProps&gt;(
  ({ $shape = "square" }) =&gt; {
    const shapeStyle = {
      round: {
        borderRadius: 100,
      },
      square: {},
    };

    return shapeStyle[$shape];
  }
);</code></pre><p>Important takeaways from the code above:</p><ol><li><p>Always add a dollar <code>$</code> or another valid sign to the beginning of your custom property. Then, use the <a href="https://mui.com/system/styled/#styled-component-options-styles-component">shouldForwardProp</a> utility function to prevent the custom prop from being forwarded to the underlying DOM element. This stops MUI from throwing <em>&#8220;Invalid attribute name: shape&#8221;</em> in the console. <a href="https://react.dev/warnings/unknown-prop">Read why unknown DOM props are bad.</a></p></li><li><p>Use <code>styled</code> a function from <code>@mui/material</code> package, not from <code>@emotion/styled</code>. Emotion has a different syntax and is not aware of all MUI components and internal structures, which Material UI adds on top of Emotion. That&#8217;s why you may run into errors or unsupported cases if you try to customize the MUI component directly with Emotion.</p></li></ol><p>Now, make sure you import your styled Button component instead of MUI&#8217;s default one everywhere you use it:</p><p><a href="https://github.com/ole-techwood/articles/blob/mui/src/App.tsx">App.tsx</a></p><pre><code>// App.tsx

import { Stack } from "@mui/material";

// Import custom styled component, not MUI's one
import { Button } from "./button.styled";

function App() {
  return (
    &lt;Stack p={2} spacing={1} sx={{ width: 100 }}&gt;
      &lt;Button variant="contained"&gt;Square&lt;/Button&gt;
      &lt;Button variant="contained" $shape="round"&gt;
        Round
      &lt;/Button&gt;
    &lt;/Stack&gt;
  );
}

export default App;</code></pre><p>The result:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0ji9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F036990f4-c43f-4395-a7fd-7e882fd1f76b_160x164.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0ji9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F036990f4-c43f-4395-a7fd-7e882fd1f76b_160x164.png 424w, https://substackcdn.com/image/fetch/$s_!0ji9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F036990f4-c43f-4395-a7fd-7e882fd1f76b_160x164.png 848w, https://substackcdn.com/image/fetch/$s_!0ji9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F036990f4-c43f-4395-a7fd-7e882fd1f76b_160x164.png 1272w, https://substackcdn.com/image/fetch/$s_!0ji9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F036990f4-c43f-4395-a7fd-7e882fd1f76b_160x164.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0ji9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F036990f4-c43f-4395-a7fd-7e882fd1f76b_160x164.png" width="160" height="164" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/036990f4-c43f-4395-a7fd-7e882fd1f76b_160x164.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:164,&quot;width&quot;:160,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:10597,&quot;alt&quot;:&quot;Two green buttons labeled &#8220;Square&#8221; and &#8220;Round,&#8221; showing different corner styles.&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;:&quot;https://techwood.substack.com/i/173835028?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F036990f4-c43f-4395-a7fd-7e882fd1f76b_160x164.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Two green buttons labeled &#8220;Square&#8221; and &#8220;Round,&#8221; showing different corner styles." title="Two green buttons labeled &#8220;Square&#8221; and &#8220;Round,&#8221; showing different corner styles." srcset="https://substackcdn.com/image/fetch/$s_!0ji9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F036990f4-c43f-4395-a7fd-7e882fd1f76b_160x164.png 424w, https://substackcdn.com/image/fetch/$s_!0ji9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F036990f4-c43f-4395-a7fd-7e882fd1f76b_160x164.png 848w, https://substackcdn.com/image/fetch/$s_!0ji9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F036990f4-c43f-4395-a7fd-7e882fd1f76b_160x164.png 1272w, https://substackcdn.com/image/fetch/$s_!0ji9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F036990f4-c43f-4395-a7fd-7e882fd1f76b_160x164.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption"><em>Two shapes of the button</em></figcaption></figure></div><p>If the theme doesn&#8217;t cover your needs, you may also be tempted to use the <code>sx</code> prop. Here&#8217;s why creating a styled component works better.</p><h3>Why not to use <code>sx</code>?</h3><p><a href="https://mui.com/material-ui/customization/how-to-customize/#the-sx-prop">The SX prop</a> is fine for quickly adding 2-3 theme-aware inline styles. But if you have more complex styling logic, consider using styled components, like I showed above. With this approach, you keep your styling and business logic separate, which improves readability and maintainability.</p><p>Sometimes customization goes beyond styling. In that case, it&#8217;s time for our third rule.</p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://techwood.substack.com/p/three-react-mui-commandments?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Enjoying the article so far? Help others love it too by spreading the word about it</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://techwood.substack.com/p/three-react-mui-commandments?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://techwood.substack.com/p/three-react-mui-commandments?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><h2><strong>Style can&#8217;t solve? React component is resolve</strong></h2><p>Pretend we have a requirement to add a sound effect every time a user clicks on the button. Styled components cannot achieve this, and MUI doesn&#8217;t provide any design tokens or capabilities for adding sound either.</p><p>When customization involves complex logic, we create a new React wrapper around the styled button:</p><p><a href="https://github.com/ole-techwood/articles/blob/mui/src/Button.tsx">Button.tsx</a></p><pre><code>// Button.tsx

import useSound from "use-sound";
import buttonSound from "../public/button.mp3";
import type { FC } from "react";
import { type ButtonProps, Button as ButtonStyled } from "./button.styled";

export const Button: FC&lt;ButtonProps&gt; = (props) =&gt; {
  const [play] = useSound(buttonSound);

  return &lt;ButtonStyled {...props} onClick={() =&gt; play()}&gt;&lt;/ButtonStyled&gt;;
};</code></pre><blockquote><p><strong>NOTE:</strong> To play sound, I use <a href="https://github.com/joshwcomeau/use-sound">useSound hook</a>. Find sound effects on <a href="https://freesound.org/">freesound.org</a>.</p></blockquote><p>Now use the new button component inside your app:</p><p><a href="https://github.com/ole-techwood/articles/blob/mui/src/App.tsx">App.tsx</a></p><pre><code>// App.tsx

import { Stack } from "@mui/material";

// Import custom wrapper component, not MUI's or styled one
import { Button } from "./Button";

function App() {
  return (
    &lt;Stack p={2} spacing={1} sx={{ width: 100 }}&gt;
      &lt;Button variant="contained"&gt;Square&lt;/Button&gt;
      &lt;Button variant="contained" $shape="round"&gt;
        Round
      &lt;/Button&gt;
    &lt;/Stack&gt;
  );
}

export default App;</code></pre><p>Now that you&#8217;ve seen all three rules in action, let&#8217;s wrap it up.</p><h2>Conclusion</h2><p>Congratulations! &#127881;</p><p>Now you know how to keep your MUI-based design system tidy. You&#8217;ve learned:</p><ol><li><p>Why and how to customize components through the global theme wherever it makes sense.</p></li><li><p>In what cases use styled components.</p></li><li><p>In what cases you should create a separate React wrapper over the MUI component.</p></li></ol><p>Without this hierarchy, you&#8217;ll end up mixing theme overrides, inline <code>sx</code>, and wrapper hacks in random places.</p><p>By applying these three easy-to-follow rules, you&#8217;re going to build design systems that are easy to maintain, test, and document. Your colleagues and future self will very likely appreciate it &#128578;</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://techwood.substack.com/p/three-react-mui-commandments/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://techwood.substack.com/p/three-react-mui-commandments/comments"><span>Leave a comment</span></a></p><h3>Want more? &#128293;</h3><p>Check my previous article &#8220;<a href="https://open.substack.com/pub/techwood/p/react-mvvm-with-tanstack-router?r=5uvuls&amp;utm_campaign=post&amp;utm_medium=web">React MVVM Architecture with TanStack Router: An Introduction</a>&#8221;, where I teach good old React MVVM with a modern tech stack.</p><div><hr></div><p>Creating content like this is not an easy ride, so I hope you&#8217;ll hit that like button and drop a comment. As usual, constructive feedback is always welcome!</p><p>&#128279; <a href="https://www.linkedin.com/in/olexandr-pylypyshyn/">Follow me on LinkedIn</a></p><p>&#128240; Read me on the platform of your choice: <a href="https://techwood.substack.com/">Substack</a>, <a href="https://dev.to/techwood">DEV.to</a></p><p>Till next time! &#128075;</p>]]></content:encoded></item><item><title><![CDATA[React MVVM Architecture with TanStack Router: Orchestrator]]></title><description><![CDATA[Learn how to connect UI and logic in React using MVVM with TanStack Router. Build maintainable, scalable apps with clear, focused layers.]]></description><link>https://techwood.substack.com/p/react-mvvm-orchestrator</link><guid isPermaLink="false">https://techwood.substack.com/p/react-mvvm-orchestrator</guid><dc:creator><![CDATA[Olexandr]]></dc:creator><pubDate>Mon, 18 Aug 2025 18:08:48 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!tZ1a!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfae02b2-0458-4353-b36a-6208878e670e_1920x1080.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tZ1a!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfae02b2-0458-4353-b36a-6208878e670e_1920x1080.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tZ1a!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfae02b2-0458-4353-b36a-6208878e670e_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!tZ1a!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfae02b2-0458-4353-b36a-6208878e670e_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!tZ1a!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfae02b2-0458-4353-b36a-6208878e670e_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!tZ1a!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfae02b2-0458-4353-b36a-6208878e670e_1920x1080.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tZ1a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfae02b2-0458-4353-b36a-6208878e670e_1920x1080.jpeg" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dfae02b2-0458-4353-b36a-6208878e670e_1920x1080.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:697154,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://techwood.substack.com/i/171296349?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfae02b2-0458-4353-b36a-6208878e670e_1920x1080.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tZ1a!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfae02b2-0458-4353-b36a-6208878e670e_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!tZ1a!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfae02b2-0458-4353-b36a-6208878e670e_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!tZ1a!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfae02b2-0458-4353-b36a-6208878e670e_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!tZ1a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfae02b2-0458-4353-b36a-6208878e670e_1920x1080.jpeg 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><figcaption class="image-caption"><em>Orchestrating component. Photo by <a href="https://unsplash.com/@andreazanenga?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Andrea Zanenga</a> on <a href="https://unsplash.com/photos/people-sitting-on-chairs-inside-building-yUJVHiYZCGQ?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></em></figcaption></figure></div><p>Earlier in <a href="https://techwood.substack.com/p/react-mvvm-with-tanstack-router">this MVVM series</a>, we discussed that blending the UI with the logic undermines maintainability and testability. So, if we cannot mix these two, how do we actually connect them?</p><p>The answer is in the final piece of the MVVM puzzle - orchestrating component (also known as the <strong>orchestrator</strong>):</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Ocjr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f8102e-f07b-44ae-a586-233848a40c8d_732x1836.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Ocjr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f8102e-f07b-44ae-a586-233848a40c8d_732x1836.png 424w, https://substackcdn.com/image/fetch/$s_!Ocjr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f8102e-f07b-44ae-a586-233848a40c8d_732x1836.png 848w, https://substackcdn.com/image/fetch/$s_!Ocjr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f8102e-f07b-44ae-a586-233848a40c8d_732x1836.png 1272w, https://substackcdn.com/image/fetch/$s_!Ocjr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f8102e-f07b-44ae-a586-233848a40c8d_732x1836.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Ocjr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f8102e-f07b-44ae-a586-233848a40c8d_732x1836.png" width="252" height="632.0655737704918" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/00f8102e-f07b-44ae-a586-233848a40c8d_732x1836.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1836,&quot;width&quot;:732,&quot;resizeWidth&quot;:252,&quot;bytes&quot;:190700,&quot;alt&quot;:&quot;Diagram of software architecture showing Orchestrator (highlighted) with Data, Model, Application, and View layers.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://techwood.substack.com/i/171296349?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f8102e-f07b-44ae-a586-233848a40c8d_732x1836.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Diagram of software architecture showing Orchestrator (highlighted) with Data, Model, Application, and View layers." title="Diagram of software architecture showing Orchestrator (highlighted) with Data, Model, Application, and View layers." srcset="https://substackcdn.com/image/fetch/$s_!Ocjr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f8102e-f07b-44ae-a586-233848a40c8d_732x1836.png 424w, https://substackcdn.com/image/fetch/$s_!Ocjr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f8102e-f07b-44ae-a586-233848a40c8d_732x1836.png 848w, https://substackcdn.com/image/fetch/$s_!Ocjr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f8102e-f07b-44ae-a586-233848a40c8d_732x1836.png 1272w, https://substackcdn.com/image/fetch/$s_!Ocjr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f8102e-f07b-44ae-a586-233848a40c8d_732x1836.png 1456w" sizes="100vw"></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><figcaption class="image-caption"><em>Orchestrating component (highlighted) in the big picture of MVVM.</em></figcaption></figure></div><blockquote><p><strong>TIP:</strong> If you haven&#8217;t read <a href="https://techwood.substack.com/p/react-mvvm-with-tanstack-router">my MVVM Introduction</a> yet, I highly recommend doing that first. It&#8217;ll help you understand where the orchestrator fits in the bigger picture.</p></blockquote><p>In this article, you&#8217;ll learn:</p><ol><li><p>What&#8217;s the orchestrating component, and what is his job.</p></li><li><p>How to create the orchestrator and combine the layers within it.</p></li><li><p>How to test the orchestrating component.</p></li></ol><p>Before we begin, I want to remind, that in this MVVM series, we&#8217;re building a country quiz app. The app prompts the user with the flag, and the user has to guess the country. Once they entered and sent the answer, we save it in localStorage. Then we display the answer history in the left sidebar. You can find <a href="https://github.com/ole-techwood/articles/tree/mvvm">the source code on GitHub</a> and the <a href="https://codesandbox.io/p/github/ole-techwood/articles/mvvm">Demo on CodeSandbox</a>.</p><h2>The glue of a module</h2><p>Orchestrator is a simple React component that has two main tasks:</p><ol><li><p>It connects UI (represented by the view layer) with the business logic (represented by the application layer).</p></li><li><p>It serves as an entry point into the module. For example, in <a href="https://tanstack.com/router/latest/docs/framework/react/routing/routing-concepts#basic-routes">TanStack Router</a>, the orchestrator can be the route&#8217;s primary component:</p><pre><code>import { Orchestrator } from "@/modules/Module";
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/")({
  component: Orchestrator,
});</code></pre></li></ol><p>Now, how do we implement the orchestrator?</p><h2>Creation of the orchestrator</h2><p>Open your IDE of choice and, inside the <code>Home</code> module, create a new file called <code>Home.tsx</code>. Your folder structure will look like this:</p><pre><code>src
&#9492;&#9472;&#9472; modules
    &#9492;&#9472;&#9472; Home
        &#9500;&#9472;&#9472; application
        &#9500;&#9472;&#9472; data
        &#9500;&#9472;&#9472; model
        &#9500;&#9472;&#9472; view
        &#9500;&#9472;&#9472; <strong>Home.tsx</strong>
        &#9492;&#9472;&#9472; <strong>index.ts</strong></code></pre><blockquote><p><strong>NOTE:</strong> <code>index.ts</code> is for re-exporting. Now when you import <code>Home</code> orchestrator, use <code>@/modules/Home</code> instead of <code>@/modules/Home/Home</code> (&#8221;Home&#8221; 2 times).</p></blockquote><p>Inside <code>Home.tsx</code> create a new functional React component:</p><pre><code>export const Home: React.FC = () =&gt; {
  
};</code></pre><p>Now let&#8217;s call <a href="https://techwood.substack.com/i/171276266/random-country-to-guess">the useQuiz hook</a> from the application layer:</p><pre><code>import { useQuiz } from "./application/useQuiz";

export const Home: React.FC = () =&gt; {
  const quizProps = useQuiz();
};</code></pre><p>Last but not least, return <a href="https://techwood.substack.com/i/171272625/let-the-users-guess-the-country">the Quiz widget</a> from the view layer:</p><pre><code>import { Container } from "@mui/material";
import { Quiz } from "./view/Quiz";
import { useQuiz } from "./application/useQuiz";

export const Home: React.FC = () =&gt; {
  const quizProps = useQuiz();

  return (
    &lt;Container sx={{ display: "flex", alignItems: "center", height: "100%" }}&gt;
      &lt;Quiz {...quizProps} /&gt;
    &lt;/Container&gt;
  );
};</code></pre><blockquote><p><strong>NOTE:</strong> I&#8217;ve also wrapped the widget in <a href="https://mui.com/material-ui/react-container/">the MUI&#8217;s Container component</a> purely for styling. The orchestrating component doesn&#8217;t depend on any UI library. Feel free to use the library of your choice or not to use any.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!bgR_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f789473-a2fc-488e-a24b-3875f4fa228d_2763x513.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!bgR_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f789473-a2fc-488e-a24b-3875f4fa228d_2763x513.png 424w, https://substackcdn.com/image/fetch/$s_!bgR_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f789473-a2fc-488e-a24b-3875f4fa228d_2763x513.png 848w, https://substackcdn.com/image/fetch/$s_!bgR_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f789473-a2fc-488e-a24b-3875f4fa228d_2763x513.png 1272w, https://substackcdn.com/image/fetch/$s_!bgR_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f789473-a2fc-488e-a24b-3875f4fa228d_2763x513.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!bgR_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f789473-a2fc-488e-a24b-3875f4fa228d_2763x513.png" width="1456" height="270" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6f789473-a2fc-488e-a24b-3875f4fa228d_2763x513.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:270,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:131950,&quot;alt&quot;:&quot;Diagram showing orchestrator passing props from useQuiz in Application Layer to Quiz widget in View Layer.&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;:&quot;https://techwood.substack.com/i/171296349?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f789473-a2fc-488e-a24b-3875f4fa228d_2763x513.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Diagram showing orchestrator passing props from useQuiz in Application Layer to Quiz widget in View Layer." title="Diagram showing orchestrator passing props from useQuiz in Application Layer to Quiz widget in View Layer." srcset="https://substackcdn.com/image/fetch/$s_!bgR_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f789473-a2fc-488e-a24b-3875f4fa228d_2763x513.png 424w, https://substackcdn.com/image/fetch/$s_!bgR_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f789473-a2fc-488e-a24b-3875f4fa228d_2763x513.png 848w, https://substackcdn.com/image/fetch/$s_!bgR_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f789473-a2fc-488e-a24b-3875f4fa228d_2763x513.png 1272w, https://substackcdn.com/image/fetch/$s_!bgR_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f789473-a2fc-488e-a24b-3875f4fa228d_2763x513.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption"><em>Orchestration flow.</em></figcaption></figure></div><p>As a cherry on top of a pie, replace the temporary mock code in the <code>src/routes/index.tsx</code> with your new <code>Home</code> orchestrator:</p><pre><code>import { Home } from "@/modules/Home";
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/")({
  component: Home,
});</code></pre><p>Congratulations! You&#8217;ve created the orchestrating component for the <code>Home</code> module. In this component, you:</p><ol><li><p>Took all the props returned by the <code>useQuiz</code> hook.</p></li><li><p>Provided the props to the <code>Quiz</code> widget.</p></li></ol><p>If you had more widgets and hooks, creating the orchestrator would follow the same steps: call the hooks and pass the results to the widgets through props.</p><p>But don&#8217;t forget about complexity and scaling aspects!</p><h3>Split into sub-orchestrators when needed</h3><p>When a module starts wiring 5+ independent hooks and widgets like this:</p><pre><code>import { Container, Stack, Divider } from "@mui/material";

// Application layer (business logic)
import { useProfile } from "./application/useProfile";
import { useStats } from "./application/useStats";
import { useActivity } from "./application/useActivity";
import { useNotifications } from "./application/useNotifications";
import { useTips } from "./application/useTips";

// View layer (UI widgets)
import { ProfileCard } from "./view/ProfileCard";
import { StatsCard } from "./view/StatsCard";
import { ActivityFeed } from "./view/ActivityFeed";
import { NotificationsList } from "./view/NotificationsList";
import { TipsPanel } from "./view/TipsPanel";

export const Dashboard: React.FC = () =&gt; {
  // 1. Call hooks (application layer)
  const profileProps = useProfile();           // e.g. { user, isLoading, error, refresh }
  const statsProps = useStats();               // e.g. { totals, delta, isLoading, error }
  const activityProps = useActivity();         // e.g. { items, isLoading, error }
  const notificationsProps = useNotifications(); // e.g. { items, markRead, isLoading, error }
  const tipsProps = useTips();                 // e.g. { tips, dismissTip, isLoading, error }

  // 2. Wire results to widgets (view layer)
  return (
    &lt;Container sx={{ py: 2 }}&gt;
      &lt;Stack spacing={2}&gt;
        &lt;ProfileCard {...profileProps} /&gt;
        &lt;Divider /&gt;
        &lt;StatsCard {...statsProps} /&gt;
        &lt;ActivityFeed {...activityProps} /&gt;
        &lt;NotificationsList {...notificationsProps} /&gt;
        &lt;TipsPanel {...tipsProps} /&gt;
      &lt;/Stack&gt;
    &lt;/Container&gt;
  );
};</code></pre><p>Consider splitting them into sub&#8209;orchestrators:</p><pre><code>import React from "react";
import { useTips } from "./application/useTips";
import { TipsPanel } from "./view/TipsPanel";

export const Tips: React.FC = () =&gt; {
  const tipsProps = useTips(); // e.g. { tips, dismissTip, isLoading, error }
  return &lt;TipsPanel {...tipsProps} /&gt;;
};</code></pre><p>Then the main orchestrating component would connect other sub-orchestrators instead of hooks and widgets directly:</p><pre><code>import { Container, Stack } from "@mui/material";
import { Tips } from "./Tips";

export const Dashboard: React.FC = () =&gt; {
  return (
    &lt;Container sx={{ py: 2 }}&gt;
      &lt;Stack spacing={2}&gt;
        {/* ...other sub&#8209;orchestrators... */}
        &lt;Tips /&gt;
      &lt;/Stack&gt;
    &lt;/Container&gt;
  );
};</code></pre><p>Okay, we&#8217;ve discussed how to create an orchestrator and split it into more granular parts if it wires 5+ widgets and hooks. Now it&#8217;s time for tests &#129514;.</p><h2>Test the orchestrator</h2><blockquote><p><strong>NOTE:</strong> As usual, I&#8217;m using <a href="https://vitest.dev/">Vitest</a> and <a href="https://testing-library.com/docs/react-testing-library/intro/">React Testing Library</a> to write unit tests.</p></blockquote><p>Test code for the orchestrating component is as simple as its actual implementation:</p><pre><code>import { render, screen } from "@testing-library/react";
import { describe, it, vi, expect, beforeEach } from "vitest";

// Capture props passed to Quiz for assertions
let lastQuizProps: any = null;

// Mock the useQuiz hook to control props returned to Home
vi.mock("./application/useQuiz", () =&gt; ({
  useQuiz: vi.fn(),
}));

// Mock Quiz view to a simple component we can assert against
vi.mock("./view/Quiz", () =&gt; ({
  Quiz: (props: any) =&gt; {
    lastQuizProps = props;
    const label = props?.country?.name ?? "no-country";
    return &lt;div data-testid="quiz"&gt;{label}&lt;/div&gt;;
  },
}));

// Imports go after mocks to ensure the mocked modules are applied before the actual modules are imported and executed.
import { Home } from "./Home";
import { useQuiz } from "./application/useQuiz";

describe("Home", () =&gt; {
  beforeEach(() =&gt; {
    lastQuizProps = null;
  });

  it("renders Quiz with props returned by useQuiz", () =&gt; {
    const mockProps = {
      country: { flag: "https://example.com/fr.png", name: "France" },
      isLoading: false,
      error: null,
      submissionError: "",
      form: {} as any,
      onSubmit: vi.fn(),
      result: undefined,
    };

    vi.mocked(useQuiz).mockReturnValue(mockProps as any);

    render(&lt;Home /&gt;);

    // Quiz should be rendered and receive the same props
    expect(!!screen.getByTestId("quiz")).toBe(true);
    expect(screen.getByTestId("quiz").textContent).toBe("France");
    expect(lastQuizProps).toMatchObject(mockProps);
  });

  it("renders Quiz even when country is null and loading is true", () =&gt; {
    const mockProps = {
      country: null,
      isLoading: true,
      error: null,
      submissionError: "",
      form: {} as any,
      onSubmit: vi.fn(),
      result: undefined,
    };

    vi.mocked(useQuiz).mockReturnValue(mockProps as any);

    render(&lt;Home /&gt;);

    // Still renders Quiz and shows our fallback label
    expect(!!screen.getByTestId("quiz")).toBe(true);
    expect(screen.getByTestId("quiz").textContent).toBe("no-country");
    expect(lastQuizProps).toMatchObject(mockProps);
  });

  it("passes error from useQuiz down to Quiz", () =&gt; {
    const error = new Error("Network failed");
    const mockProps = {
      country: null,
      isLoading: false,
      error,
      submissionError: "",
      form: {} as any,
      onSubmit: vi.fn(),
      result: undefined,
    };

    vi.mocked(useQuiz).mockReturnValue(mockProps as any);

    render(&lt;Home /&gt;);

    // Quiz is rendered and receives the error prop
    expect(!!screen.getByTestId("quiz")).toBe(true);
    expect(lastQuizProps.error).toBe(error);
  });
});</code></pre><p>In our case, testing the orchestrating component is just passing props from hooks to widgets. No internal states or side effects. Easy and clean, just like we all enjoy!</p><blockquote><p><strong>NOTE:</strong> My task is to show you the basic principles of testing the orchestrator. I&#8217;m not focused on creating comprehensive production-ready test cases. You can add more tests for better coverage.</p></blockquote><p>Testing is done! If you read my previous articles in this series, you&#8217;ve probably already guessed what&#8217;s coming next &#128578;.</p><h2>Homework</h2><p>Create a <code>History</code> orchestrator inside the <code>History</code> module. The orchestrator should:</p><ul><li><p>Take the object returned from the <code>useHistoryList</code> hook. This is the hook you had to create as homework in <a href="https://techwood.substack.com/i/171276266/homework">the application layer article.</a></p></li><li><p>Provide the object through props to the <code>HistoryList</code> widget. This is the widget you had to create as homework in <a href="https://techwood.substack.com/i/171272625/study-by-practicing">the view layer article.</a></p></li></ul><p>If you get stuck, check <a href="https://github.com/ole-techwood/articles/tree/mvvm/src/modules/History">the ready task</a> in the source code.</p><h2>MVVM brainteaser is done!</h2><p>Wow, just wow&#8230;</p><p>I can&#8217;t believe you&#8217;ve reached this point &#129401;</p><p>You&#8217;ve not only studied the orchestrator component, you&#8217;ve finished the entire MVVM journey! By finishing this article and the whole series, you&#8217;ve learned:</p><ol><li><p>How to wire the logic with the UI in the right way.</p></li><li><p>How to build more maintainable frontend applications with a clear separation of concerns.</p></li><li><p>How easy it is to test the React applications when each layer has 1 concrete task.</p></li></ol><h3>What&#8217;s next?</h3><p>Go and build something cool with your new knowledge! &#128516;</p><p>Or you can always revisit the previous articles, in case you forgot some details:</p><ul><li><p><a href="https://techwood.substack.com/p/react-mvvm-with-tanstack-router">React MVVM Architecture with TanStack Router: An Introduction</a></p></li><li><p><a href="https://techwood.substack.com/p/react-mvvm-data-layer">React MVVM Architecture with TanStack Router: Data Layer</a></p></li><li><p><a href="https://techwood.substack.com/p/react-mvvm-model-layer">React MVVM Architecture with TanStack Router: Model Layer</a></p></li><li><p><a href="https://techwood.substack.com/p/react-mvvm-view-layer">React MVVM Architecture with TanStack Router: View Layer</a></p></li><li><p><a href="https://techwood.substack.com/p/react-mvvm-application-layer">React MVVM Architecture with TanStack Router: Application Layer</a></p></li></ul><div><hr></div><p>Creating content like this is not an easy ride, so I hope you&#8217;ll hit that like button and drop a comment. As usual, constructive feedback is always welcome!</p><p>&#128279; <a href="https://www.linkedin.com/in/olexandr-pylypyshyn/">Follow me on LinkedIn</a></p><p>&#128240; Read me on the platform of your choice: <a href="https://techwood.substack.com/">Substack</a>, <a href="https://dev.to/techwood">DEV.to</a></p><p>See ya soon &#128075;</p><p></p><p></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[React MVVM Architecture with TanStack Router: Application Layer]]></title><description><![CDATA[Learn how to structure business logic in React MVVM apps using TanStack Router, Query & Form for scalable, maintainable TypeScript solutions.]]></description><link>https://techwood.substack.com/p/react-mvvm-application-layer</link><guid isPermaLink="false">https://techwood.substack.com/p/react-mvvm-application-layer</guid><dc:creator><![CDATA[Olexandr]]></dc:creator><pubDate>Mon, 18 Aug 2025 17:42:03 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!lFkO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9cd4eee-e7c0-4272-8716-58966baed1e6_1920x1280.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lFkO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9cd4eee-e7c0-4272-8716-58966baed1e6_1920x1280.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lFkO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9cd4eee-e7c0-4272-8716-58966baed1e6_1920x1280.jpeg 424w, https://substackcdn.com/image/fetch/$s_!lFkO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9cd4eee-e7c0-4272-8716-58966baed1e6_1920x1280.jpeg 848w, https://substackcdn.com/image/fetch/$s_!lFkO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9cd4eee-e7c0-4272-8716-58966baed1e6_1920x1280.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!lFkO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9cd4eee-e7c0-4272-8716-58966baed1e6_1920x1280.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lFkO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9cd4eee-e7c0-4272-8716-58966baed1e6_1920x1280.jpeg" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e9cd4eee-e7c0-4272-8716-58966baed1e6_1920x1280.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:547651,&quot;alt&quot;:&quot;Upward view of tall glass skyscrapers with office lights on against a cloudy sky.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://techwood.substack.com/i/171276266?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9cd4eee-e7c0-4272-8716-58966baed1e6_1920x1280.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Upward view of tall glass skyscrapers with office lights on against a cloudy sky." title="Upward view of tall glass skyscrapers with office lights on against a cloudy sky." srcset="https://substackcdn.com/image/fetch/$s_!lFkO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9cd4eee-e7c0-4272-8716-58966baed1e6_1920x1280.jpeg 424w, https://substackcdn.com/image/fetch/$s_!lFkO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9cd4eee-e7c0-4272-8716-58966baed1e6_1920x1280.jpeg 848w, https://substackcdn.com/image/fetch/$s_!lFkO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9cd4eee-e7c0-4272-8716-58966baed1e6_1920x1280.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!lFkO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9cd4eee-e7c0-4272-8716-58966baed1e6_1920x1280.jpeg 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><figcaption class="image-caption"><em>Business logic. Photo by <a href="https://unsplash.com/@seanpollock?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Sean Pollock</a> on <a href="https://unsplash.com/photos/low-angle-photo-of-city-high-rise-buildings-during-daytime-PhYq704ffdA?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></em></figcaption></figure></div><p>When we build complex, feature-rich React applications, we tend to break the codebase into smaller components. But the problem is that as our application grows, so do our components.</p><p>At some point, you realize it&#8217;s impossible to read, maintain, or test the enormous Frankenstein that blends UI, business, and data access logic. A lot of the components like this lead to bugs, slowing down feature delivery and new devs&#8217; onboarding difficulties.</p><p>In the previous articles, we&#8217;ve already moved UI and data parts to their appropriate layers:</p><ul><li><p><a href="https://techwood.substack.com/p/react-mvvm-data-layer">React MVVM Architecture with TanStack Router: Data Layer</a></p></li><li><p><a href="https://techwood.substack.com/p/react-mvvm-view-layer">React MVVM Architecture with TanStack Router: View Layer</a></p></li></ul><p>Today, we study the application layer, which handles the business logic within the module:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TBtn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71378099-46a4-4aab-ad56-bffaedffa986_720x1830.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TBtn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71378099-46a4-4aab-ad56-bffaedffa986_720x1830.png 424w, https://substackcdn.com/image/fetch/$s_!TBtn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71378099-46a4-4aab-ad56-bffaedffa986_720x1830.png 848w, https://substackcdn.com/image/fetch/$s_!TBtn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71378099-46a4-4aab-ad56-bffaedffa986_720x1830.png 1272w, https://substackcdn.com/image/fetch/$s_!TBtn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71378099-46a4-4aab-ad56-bffaedffa986_720x1830.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TBtn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71378099-46a4-4aab-ad56-bffaedffa986_720x1830.png" width="250" height="635.4166666666666" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/71378099-46a4-4aab-ad56-bffaedffa986_720x1830.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1830,&quot;width&quot;:720,&quot;resizeWidth&quot;:250,&quot;bytes&quot;:182924,&quot;alt&quot;:&quot;Diagram of React MVVM architecture layers: Data (Repositories), Model (DTO, Module Type, Prop types), Application (highlighted) (Hooks), View (Widgets).&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://techwood.substack.com/i/171276266?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71378099-46a4-4aab-ad56-bffaedffa986_720x1830.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Diagram of React MVVM architecture layers: Data (Repositories), Model (DTO, Module Type, Prop types), Application (highlighted) (Hooks), View (Widgets)." title="Diagram of React MVVM architecture layers: Data (Repositories), Model (DTO, Module Type, Prop types), Application (highlighted) (Hooks), View (Widgets)." srcset="https://substackcdn.com/image/fetch/$s_!TBtn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71378099-46a4-4aab-ad56-bffaedffa986_720x1830.png 424w, https://substackcdn.com/image/fetch/$s_!TBtn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71378099-46a4-4aab-ad56-bffaedffa986_720x1830.png 848w, https://substackcdn.com/image/fetch/$s_!TBtn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71378099-46a4-4aab-ad56-bffaedffa986_720x1830.png 1272w, https://substackcdn.com/image/fetch/$s_!TBtn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71378099-46a4-4aab-ad56-bffaedffa986_720x1830.png 1456w" sizes="100vw"></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><figcaption class="image-caption"><em>Application layer (highlighted) in the big picture of MVVM.</em></figcaption></figure></div><blockquote><p><strong>TIP:</strong> If you haven&#8217;t read <a href="https://techwood.substack.com/p/react-mvvm-with-tanstack-router">my MVVM Introduction</a> yet, I highly recommend doing that first. It&#8217;ll help you understand where the application layer fits in the bigger picture.</p></blockquote><p>In this article, you&#8217;ll learn:</p><ol><li><p>How to utilize custom hooks for business logic handling.</p></li><li><p>How to validate the data we send and receive.</p></li><li><p>How to handle forms using <a href="https://tanstack.com/form/latest/docs/overview">TanStack Form</a>. I introduced the library in <a href="https://techwood.substack.com/p/react-mvvm-view-layer">the view layer article.</a></p></li></ol><p>Before we begin, I want to remind, that in this MVVM series, we&#8217;re building a country quiz app. The app prompts the user with the flag, and the user has to guess the country. Once they entered and sent the answer, we save it in localStorage. Then we display the answer history in the left sidebar. You can find <a href="https://github.com/ole-techwood/articles/tree/mvvm">the source code on GitHub</a> and the <a href="https://codesandbox.io/p/github/ole-techwood/articles/mvvm">Demo on CodeSandbox</a>.</p><h2>Random country to guess</h2><p>In <a href="https://techwood.substack.com/i/171214223/your-first-repository">the data layer article</a>, we fetched all the available countries from the <a href="https://restcountries.com/">REST Countries</a> service using the <code>findCountries</code> method of <code>HomeRepository</code>. The response object of the method looks like this:</p><pre><code>type CountryPayload = {
    flags: {
        png: string;
        svg: string;
        alt: string;
    };
    name: {
        common: string;
        official: string;
        nativeName: Record&lt;string, {
            official: string;
            common: string;
        }&gt;;
   };
}</code></pre><p>Obviously, we don&#8217;t need all the fields in our simple app. It&#8217;ll be enough to have the next object:</p><pre><code>type Country = {
  flag: string;
  name: string;
}</code></pre><p>To get such an object, we need to map the data coming from the <code>findCountries</code> method. Let&#8217;s create the application layer for the <code>Home</code> module and add a custom hook called <code>useQuiz</code>:</p><pre><code>// src/modules/Home/application/useQuiz.ts

import { useSuspenseQuery } from "@tanstack/react-query";
import { useMemo, useState } from "react";
import { homeRepositoryFactory } from "../data";

export const useQuiz = () =&gt; {
&#9;const homeRepository = homeRepositoryFactory();
  const historyRepository = historyRepositoryFactory();

&#9;// 1. Load the data
&#9;const {
    data: countries,
    isLoading,
    error,
  } = useSuspenseQuery({
    ...homeRepository.findCountries(),
    // 2. Map the data to the desired structure
    select: (countries) =&gt;
      countries.data.map((country) =&gt; ({
        flag: country.flags.png,
        name: country.name.common,
      })),
  });
  
  // 3. Select one random country
  // This may occasionally give the same country again. You can customize the randomizer to exclude the current one if needed.
  const randomCountry = useMemo(
    // TODO: Add empty-array guard, just in case the rest countries provider ever returns empty array
    () =&gt; countries[Math.floor(Math.random() * countries.length)],
    [countries]
  );

&#9;// 4. State with one random country. We'll use it when we submit the form
  const [country, setCountry] = useState(randomCountry);
  
  // We rely on TypeScript to infer the return type of useQuiz automatically.
  return { country, isLoading, error };
}</code></pre><p>Let&#8217;s break the hook down:</p><ol><li><p>First, we load the data from the repository using <code>useSuspenseQuery</code>. The <code>findCountries</code> returns the result of the <code>queryOptions</code> method from TanStack Query, so we can spread it into <code>useSuspenseQuery</code>.</p><blockquote><p><strong>NOTE:</strong> Dive deeper into <a href="https://techwood.substack.com/i/171214223/what-is-a-repository">the repository pattern</a> described in the data layer article.</p></blockquote></li><li><p>Second, we map the data from the repository utilizing the <a href="https://tanstack.com/query/latest/docs/framework/react/guides/render-optimizations#select">select</a> method. The method is also provided by the TanStack Query.</p></li><li><p>Then we select one random country from the array of mapped countries.</p></li><li><p>Last but not least, we save the random country in the state, so we can change the country later when the user presses the &#8220;Next country&#8221; button.</p></li></ol><p>We&#8217;ve mapped the complex object into one that is much easier to work with. But imagine the third-party API provider changed the response signature. Now it has <code>flagImages</code> key instead of <code>flags</code>. If we try to map our data, we&#8217;ll get annoying &#8220;<em>TypeError: Cannot read properties of undefined&#8221;</em></p><h2>Wrong data shall not pass</h2><p>To avoid situations above, I&#8217;ve introduced <a href="https://zod.dev/">Zod</a> in <a href="https://techwood.substack.com/i/171264907/payload-typing-the-api-response">the model layer article</a>. Before we map the data, we should validate it in the <a href="https://tanstack.com/query/latest/docs/framework/react/guides/query-functions">queryFn</a> method:</p><pre><code>import { countriesPayload } from "../model";

export const useQuiz = () =&gt; {
&#9;const homeRepository = homeRepositoryFactory();
  const historyRepository = historyRepositoryFactory();

&#9;const {
    data: countries,
    isLoading,
    error,
  } = useSuspenseQuery({
    ...homeRepository.findCountries(),
    queryFn: async (context) =&gt; {
      const queryFn = homeRepository.findCountries().queryFn;

      if (!queryFn) {
        throw new Error(
          "Unexpected error happened during loading the countries. Please retry after 15 minutes."
        );
      }

      const countries = await queryFn(context);

      // Parse the data from the repository
      const payload = countriesPayload.safeParse(countries.data);

      // In Suspense mode, thrown errors are caught by your error boundary
      if (!payload.success) {
        throw new Error(
          "We've received an incorrect data from our countries provider. We're already fixing this error, please retry after 15 minutes.",
          { cause: payload.error }
        );
      }

      return countries;
    },
&#9;&#9;// ...
  });
  // ...
}</code></pre><p>Let&#8217;s go through the process step by step:</p><ol><li><p>First, we parse the data from the repository using Zod&#8217;s <a href="https://zod.dev/basics?id=handling-errors">safeParse</a> method.</p></li><li><p>Next, if the actual data and its Zod signature don&#8217;t match, we immediately throw an error. Then your error boundary catches it.</p></li></ol><blockquote><p><strong>NOTE:</strong> We put validation in <code>queryFn</code> because it runs before <code>select</code>, ensuring only valid data reaches the mapping step.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0_8A!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F912ad3f1-0dd0-43b9-beb4-a77e1f360a13_2913x630.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0_8A!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F912ad3f1-0dd0-43b9-beb4-a77e1f360a13_2913x630.png 424w, https://substackcdn.com/image/fetch/$s_!0_8A!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F912ad3f1-0dd0-43b9-beb4-a77e1f360a13_2913x630.png 848w, https://substackcdn.com/image/fetch/$s_!0_8A!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F912ad3f1-0dd0-43b9-beb4-a77e1f360a13_2913x630.png 1272w, https://substackcdn.com/image/fetch/$s_!0_8A!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F912ad3f1-0dd0-43b9-beb4-a77e1f360a13_2913x630.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0_8A!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F912ad3f1-0dd0-43b9-beb4-a77e1f360a13_2913x630.png" width="1456" height="315" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/912ad3f1-0dd0-43b9-beb4-a77e1f360a13_2913x630.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:315,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:185635,&quot;alt&quot;:&quot;Diagram of data flow: Data Layer Repositories &#8594; Application Layer (queryFn, select) &#8594; View Layer Widgets.&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;:&quot;https://techwood.substack.com/i/171276266?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F912ad3f1-0dd0-43b9-beb4-a77e1f360a13_2913x630.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Diagram of data flow: Data Layer Repositories &#8594; Application Layer (queryFn, select) &#8594; View Layer Widgets." title="Diagram of data flow: Data Layer Repositories &#8594; Application Layer (queryFn, select) &#8594; View Layer Widgets." srcset="https://substackcdn.com/image/fetch/$s_!0_8A!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F912ad3f1-0dd0-43b9-beb4-a77e1f360a13_2913x630.png 424w, https://substackcdn.com/image/fetch/$s_!0_8A!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F912ad3f1-0dd0-43b9-beb4-a77e1f360a13_2913x630.png 848w, https://substackcdn.com/image/fetch/$s_!0_8A!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F912ad3f1-0dd0-43b9-beb4-a77e1f360a13_2913x630.png 1272w, https://substackcdn.com/image/fetch/$s_!0_8A!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F912ad3f1-0dd0-43b9-beb4-a77e1f360a13_2913x630.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption"><em>Application layer validates and maps the repository&#8217;s raw data to display it on the UI.</em></figcaption></figure></div><p>Data from the repository is mapped and validated! Now it&#8217;s time to get the user&#8217;s answer, validate it, and save it.</p><h2>Form submission</h2><p>As I stated previously, to handle the form, we use TanStack Form. Inside the application layer, we must initialize the form using <a href="https://tanstack.com/form/latest/docs/framework/react/guides/basic-concepts#form-instance">the useForm hook</a>:</p><pre><code>import { createHistoryRecordDTO, type CreateHistoryRecordDTO } from "@/modules/History/model";
import { historyRepositoryFactory } from "@/modules/History/data";
import {useForm} from '@tanstack/react-form'
import z from "zod";

export const useQuiz = () =&gt; {
  const historyRepository = historyRepositoryFactory();

  //...
  // 1. States with the result and error to check and display them in the view
  const [result, setResult] = useState &lt; CreateHistoryRecordDTO &gt; ();
  const [submissionError, setSubmissionError] = useState("");

  // 2. Form instance initialization, answer is one default empty string value
  const form = useForm({
    defaultValues: {
      answer: ""
    },
    // 3. Specify form validator
    validators: {
      onChange: ({
        value
      }) =&gt; ({
        fields: {
          answer: !value.answer ? "You cannot submit empty answer" : undefined,
        },
      }),
    },
    // 4. Submission logic
    onSubmit({
      value
    }) {
      // a. Create a DTO object
      const dto = {
        flagImage: country.flag,
        countryName: country.name,
        userAnswer: value.answer,
      };

      // b. Parse DTO
      const historyDto = createHistoryRecordDTO.safeParse(dto);

      if (!historyDto.success) {
        // c. https://zod.dev/error-formatting#zflattenerror
        const error = z.flattenError(
          historyDto.error as unknown as z.ZodError &lt; CreateHistoryRecordDTO &gt;
        );

        // d. Retrieve the first error
        const formError = error.formErrors[0];
        const firstFieldError = Object.values(error.fieldErrors)[0][0];

        // e. Set the error
        setSubmissionError(formError || firstFieldError);

        return;
      }

      // f. Set the result
      setResult(dto);

      // g. Save the result
      // TODO: saving to localStorage can fail, add try/catch if you need it
      historyRepository.saveAnswer(historyDto.data);
    },
  });
  //...

  return {
    country,
    isLoading,
    form,
    result,
    error,
    submissionError
  };
}</code></pre><p>Pretty a lot, I know, but nothing difficult here:</p><ol><li><p>First, we create a local state with the submission result and error so the view can display them.</p></li><li><p>Next, we initialize the new form instance with an <code>answer</code> empty string as the default.</p></li><li><p>Third, we specify a form validator. This one simply checks whether the submitted answer isn&#8217;t empty. Form validator has nothing to do with Zod&#8217;s one. Let&#8217;s quickly compare them to understand the difference:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_TZ9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa096f9e4-1cfd-47e6-8094-a267cc0f89f9_1320x536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_TZ9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa096f9e4-1cfd-47e6-8094-a267cc0f89f9_1320x536.png 424w, https://substackcdn.com/image/fetch/$s_!_TZ9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa096f9e4-1cfd-47e6-8094-a267cc0f89f9_1320x536.png 848w, https://substackcdn.com/image/fetch/$s_!_TZ9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa096f9e4-1cfd-47e6-8094-a267cc0f89f9_1320x536.png 1272w, https://substackcdn.com/image/fetch/$s_!_TZ9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa096f9e4-1cfd-47e6-8094-a267cc0f89f9_1320x536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_TZ9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa096f9e4-1cfd-47e6-8094-a267cc0f89f9_1320x536.png" width="1320" height="536" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a096f9e4-1cfd-47e6-8094-a267cc0f89f9_1320x536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:536,&quot;width&quot;:1320,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:111345,&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;:&quot;https://techwood.substack.com/i/171276266?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa096f9e4-1cfd-47e6-8094-a267cc0f89f9_1320x536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_TZ9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa096f9e4-1cfd-47e6-8094-a267cc0f89f9_1320x536.png 424w, https://substackcdn.com/image/fetch/$s_!_TZ9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa096f9e4-1cfd-47e6-8094-a267cc0f89f9_1320x536.png 848w, https://substackcdn.com/image/fetch/$s_!_TZ9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa096f9e4-1cfd-47e6-8094-a267cc0f89f9_1320x536.png 1272w, https://substackcdn.com/image/fetch/$s_!_TZ9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa096f9e4-1cfd-47e6-8094-a267cc0f89f9_1320x536.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></li></ol><ol start="4"><li><p>Last but not least, we define submission logic:</p><ol><li><p>Create a DTO object that we&#8217;ll save in localStorage.</p></li><li><p>Parse and validate the DTO.</p></li><li><p>If validation <em>isn&#8217;t</em> successful, <a href="https://zod.dev/error-formatting#zflattenerror">we take the first error</a> and set it into <code>submissionError</code> state.</p></li><li><p>If validation <em>is</em> successful, we set the result into the state and save it in localStorage through the <code>HistoryRepository</code>.</p></li></ol></li></ol><h3>Triggering the form submission</h3><p>Okay, we&#8217;ve defined the submission logic, but how do we trigger it? A piece of cake:</p><pre><code>import { useCallback, useMemo, useState } from "react";

export const useQuiz = () =&gt; {
  //...
&#9;
  const onSubmit = useCallback(() =&gt; {
    if (!result) {
      form.handleSubmit();
    } else {
      form.resetField("answer");
      setCountry(randomCountry);
      setResult(undefined);
    }
  }, [form, result, randomCountry]);
&#9;
  //...
&#9;
  return { country, isLoading, form, result, error, onSubmit, submissionError };
}</code></pre><p>As usual, let&#8217;s deconstruct what&#8217;s going on:</p><ol><li><p>If there&#8217;s no result yet, we submit the form.</p></li><li><p>If there&#8217;s a result, we:</p><ol><li><p>Reset the <code>answer</code> form field.</p></li><li><p>Set a new random country.</p></li><li><p>Set the result to <code>undefined</code> to be able to submit the form again.</p></li></ol></li></ol><p>And that&#8217;s it! We&#8217;ve successfully created the custom <code>useQuiz</code> hook that handles all the business logic required to make the quiz work.</p><p>But the job is not fully done yet. Remember, in the view layer article, <a href="https://techwood.substack.com/i/171272625/let-the-users-guess-the-country">we defined a QuizProps for the Quiz widget</a>? Now it&#8217;s time to connect the <code>useQuiz</code> with the <code>QuizProps</code> like this:</p><pre><code>import type { useQuiz } from "../application/useQuiz";

export type QuizProps = ReturnType&lt;typeof useQuiz&gt;;</code></pre><p>This applies <a href="https://techwood.substack.com/i/171264907/prop-types-provide-data-or-configuration-to-ui">the trick we&#8217;ve discussed in the model layer</a> article in practice.</p><p>With the hook created and props tweaked, it&#8217;s time to ensure the hook works as expected.</p><h2>Testing</h2><blockquote><p><strong>NOTE:</strong> As always, I&#8217;m using <a href="https://vitest.dev/">Vitest</a> and <a href="https://testing-library.com/docs/react-testing-library/intro/">React Testing Library</a> to write unit tests.</p></blockquote><p>Tests for <code>useQuiz</code> look like this:</p><pre><code>import { renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useQuiz } from "../useQuiz";

// Mock dependencies
vi.mock("@/modules/History/data", () =&gt; ({
  historyRepositoryFactory: vi.fn(() =&gt; ({
    saveAnswer: vi.fn(),
  })),
}));

vi.mock("../../data", () =&gt; ({
  homeRepositoryFactory: vi.fn(() =&gt; ({
    findCountries: vi.fn(() =&gt; ({
      queryKey: ["countries"],
      queryFn: vi.fn(),
    })),
  })),
}));

vi.mock("@/modules/History/model", () =&gt; ({
  createHistoryRecordDTO: {
    safeParse: vi.fn(),
  },
}));

vi.mock("../../model", () =&gt; ({
  countriesPayload: {
    safeParse: vi.fn(),
  },
}));

vi.mock("@tanstack/react-query", () =&gt; ({
  useSuspenseQuery: vi.fn(),
}));

vi.mock("@tanstack/react-form", () =&gt; ({
  useForm: vi.fn(),
}));

describe("useQuiz Hook", () =&gt; {
  const mockRawCountriesResponse = {
    data: [
      {
        flags: { png: "https://example.com/fr.png" },
        name: { common: "France" },
      },
      {
        flags: { png: "https://example.com/de.png" },
        name: { common: "Germany" },
      },
    ],
  };

  const mockTransformedCountries = [
    { flag: "https://example.com/fr.png", name: "France" },
    { flag: "https://example.com/de.png", name: "Germany" },
  ];

  beforeEach(async () =&gt; {
    // Mock react-query - now returns already transformed data from queryFn
    const reactQuery = await import("@tanstack/react-query");
    vi.mocked(reactQuery.useSuspenseQuery).mockReturnValue({
      data: mockTransformedCountries,
      isLoading: false,
      error: null,
    } as any);

    // Mock react-form
    const reactForm = await import("@tanstack/react-form");
    vi.mocked(reactForm.useForm).mockReturnValue({
      Field: vi.fn(),
      handleSubmit: vi.fn(),
      resetField: vi.fn(),
      setFieldValue: vi.fn(),
      useField: vi.fn(() =&gt; ({
        state: { value: "", meta: { errors: [] } },
      })),
    } as any);

    // Mock model parsers - these are now used in the repository's queryFn
    const model = await import("../../model");
    vi.mocked(model.countriesPayload.safeParse).mockReturnValue({
      success: true,
      data: mockRawCountriesResponse.data,
    } as any);

    const historyModel = await import("@/modules/History/model");
    vi.mocked(historyModel.createHistoryRecordDTO.safeParse).mockReturnValue({
      success: true,
      data: {
        flagImage: "https://example.com/fr.png",
        countryName: "France",
        userAnswer: "france",
      },
    } as any);

    vi.clearAllMocks();
  });

  describe("VALID SCENARIOS", () =&gt; {
    it("should initialize with default values and random country", () =&gt; {
      const { result } = renderHook(() =&gt; useQuiz());

      expect(result.current.country).toBeDefined();
      expect(result.current.isLoading).toBe(false);
      expect(result.current.error).toBeNull();
      expect(result.current.submissionError).toBe("");
    });
    
    // Find the rest of the tests here: https://github.com/ole-techwood/articles/tree/mvvm/src/modules/Home/application/tests
  });

  describe("BOUNDARY CONDITIONS", () =&gt; {
    it("should handle single country in dataset", async () =&gt; {
      const singleCountry = [
        { flag: "https://example.com/fr.png", name: "France" },
      ];

      const reactQuery = await import("@tanstack/react-query");
      vi.mocked(reactQuery.useSuspenseQuery).mockReturnValue({
        data: singleCountry,
        isLoading: false,
        error: null,
      } as any);

      const { result } = renderHook(() =&gt; useQuiz());

      expect(result.current.country).toEqual(singleCountry[0]);
    });
    
    // Find the rest of the tests here: https://github.com/ole-techwood/articles/tree/mvvm/src/modules/Home/application/tests
  });

  describe("EDGE CASES", () =&gt; {
    it("should maintain state consistency during re-renders", () =&gt; {
      const { result, rerender } = renderHook(() =&gt; useQuiz());

      const initialCountry = result.current.country;
      rerender();

      expect(result.current.country).toBeDefined();
      expect(result.current.country).toEqual(initialCountry);
    });
    
    // Find the rest of the tests here: https://github.com/ole-techwood/articles/tree/mvvm/src/modules/Home/application/tests
  });
});
</code></pre><p>Yes, business logic remains the hardest part of the app to test. You have to mock a lot, and you have to think about many cases. But, hey, at least now you test your business logic in isolation from other layers of the app. This approach makes your mocks more predictable and easier.</p><blockquote><p><strong>NOTE:</strong> Keep in mind that I&#8217;ve tested only basic valid and edge conditions to show you the principle of testing the application layer. You might need to expand the test coverage in the production-grade systems.</p></blockquote><p>With that said, we&#8217;ve finished testing the application layer. And now, as in the previous articles, I want you to practice a bit.</p><h2>Homework</h2><p>Create a new <code>useHistoryList</code> hook that will correspond to <code>HistoryList</code> view you had to build as <a href="https://techwood.substack.com/i/171272625/study-by-practicing">the practice task in the view layer article</a><a href="https://www.notion.so/React-MVVM-Architecture-with-TanStack-Router-View-Layer-22aadb6f0c3e8099b597f83ce46b6806?pvs=21">.</a> The hook should:</p><ol><li><p>Fetch the history from localStorage using the <code>getHistory</code> method of the <code>HistoryRepository</code>.</p></li><li><p>Validate the history array.</p></li><li><p>Return the history or the error.</p></li></ol><p>As usual, you can find <a href="https://github.com/ole-techwood/articles/tree/mvvm/src/modules/History/application">the ready assignment</a> in the source code.</p><h2>Conclusion</h2><p>You nailed it! <code>&#128640;</code></p><p>By this point, you&#8217;ve mastered business logic handling:</p><ol><li><p>You&#8217;ve learned where and how to organize the business logic.</p></li><li><p>Together, we&#8217;ve considered the most common tasks for the application layer: form handling, data loading, mapping, and validation.</p></li><li><p>At the end, we&#8217;ve tested the business logic to make sure it works as expected.</p></li></ol><p>With such a strong foundation, you&#8217;re now capable of implementing business logic of any complexity in your projects. All the best with that!</p><h3>Further reading</h3><p>You&#8217;re almost at the end of our MVVM journey! Read the last article of the series:</p><ul><li><p><a href="https://techwood.substack.com/p/react-mvvm-orchestrator">React MVVM Architecture with TanStack Router: Orchestrator</a></p></li></ul><p>Or you can go and revisit the previous ones:</p><ul><li><p><a href="https://techwood.substack.com/p/react-mvvm-with-tanstack-router">React MVVM Architecture with TanStack Router: An Introduction</a></p></li><li><p><a href="https://techwood.substack.com/p/react-mvvm-data-layer">React MVVM Architecture with TanStack Router: Data Layer</a></p></li><li><p><a href="https://techwood.substack.com/p/react-mvvm-model-layer">React MVVM Architecture with TanStack Router: Model Layer</a></p></li><li><p><a href="https://techwood.substack.com/p/react-mvvm-view-layer">React MVVM Architecture with TanStack Router: View Layer</a></p></li></ul><h3>Want more?</h3><p>TkDodo wrote <a href="https://tkdodo.eu/blog/react-query-selectors-supercharged">React Query Selectors, Supercharged</a>, in case you want to delve really deep into results transformation in TanStack Query.</p><div><hr></div><p>Creating content like this is not an easy ride, so I hope you&#8217;ll hit that like button and drop a comment. As usual, constructive feedback is always welcome!</p><p>&#128279; <a href="https://www.linkedin.com/in/olexandr-pylypyshyn/">Follow me on LinkedIn</a></p><p>&#128240; Read me on the platform of your choice: <a href="https://techwood.substack.com/">Substack</a>, <a href="https://dev.to/techwood">DEV.to</a></p><p>See ya soon &#128075;</p>]]></content:encoded></item><item><title><![CDATA[React MVVM Architecture with TanStack Router: View Layer]]></title><description><![CDATA[Learn how to build testable, maintainable React components using MVVM architecture and TanStack Router. Cleanly separate UI from logic for scalable apps.]]></description><link>https://techwood.substack.com/p/react-mvvm-view-layer</link><guid isPermaLink="false">https://techwood.substack.com/p/react-mvvm-view-layer</guid><dc:creator><![CDATA[Olexandr]]></dc:creator><pubDate>Mon, 18 Aug 2025 13:48:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!9PM0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabd86a61-2eda-45cf-af6e-9f4003f8c504_1920x1080.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9PM0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabd86a61-2eda-45cf-af6e-9f4003f8c504_1920x1080.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9PM0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabd86a61-2eda-45cf-af6e-9f4003f8c504_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!9PM0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabd86a61-2eda-45cf-af6e-9f4003f8c504_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!9PM0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabd86a61-2eda-45cf-af6e-9f4003f8c504_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!9PM0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabd86a61-2eda-45cf-af6e-9f4003f8c504_1920x1080.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9PM0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabd86a61-2eda-45cf-af6e-9f4003f8c504_1920x1080.jpeg" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/abd86a61-2eda-45cf-af6e-9f4003f8c504_1920x1080.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:511064,&quot;alt&quot;:&quot;Scenic mountain landscape with a lake, forested hills, and snow-capped peaks under a bright blue sky.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://techwood.substack.com/i/171272625?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabd86a61-2eda-45cf-af6e-9f4003f8c504_1920x1080.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Scenic mountain landscape with a lake, forested hills, and snow-capped peaks under a bright blue sky." title="Scenic mountain landscape with a lake, forested hills, and snow-capped peaks under a bright blue sky." srcset="https://substackcdn.com/image/fetch/$s_!9PM0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabd86a61-2eda-45cf-af6e-9f4003f8c504_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!9PM0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabd86a61-2eda-45cf-af6e-9f4003f8c504_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!9PM0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabd86a61-2eda-45cf-af6e-9f4003f8c504_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!9PM0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabd86a61-2eda-45cf-af6e-9f4003f8c504_1920x1080.jpeg 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><figcaption class="image-caption">View. Photo by <a href="https://unsplash.com/@pueblovista?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Paul Pastourmatzis</a> on <a href="https://unsplash.com/photos/aerial-photography-of-pine-trees-on-mountain-hKOPVtGQZ4o?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></figcaption></figure></div><p>A clear separation between UI and business logic is absolutely crucial. It makes testing and maintaining your React components much easier. This is something I scrambled with a lot in the past. I would even avoid writing unit tests, because it was so hard to mock everything in some enormous component that included JSX, hooks, data fetching, etc., all in one.</p><p>That&#8217;s why I started delving into the clean architecture in React. After years of research, trial and error, I finally found the approach that helps me to keep my components simple, yet rich in UI and features.</p><p>Today, we&#8217;re talking about the view layer of this <a href="https://techwood.substack.com/p/react-mvvm-with-tanstack-router">five-layer MVVM implementation</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_!xLd9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d271eb-34d4-4715-b5b0-2494dd16589b_720x1830.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xLd9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d271eb-34d4-4715-b5b0-2494dd16589b_720x1830.png 424w, https://substackcdn.com/image/fetch/$s_!xLd9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d271eb-34d4-4715-b5b0-2494dd16589b_720x1830.png 848w, https://substackcdn.com/image/fetch/$s_!xLd9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d271eb-34d4-4715-b5b0-2494dd16589b_720x1830.png 1272w, https://substackcdn.com/image/fetch/$s_!xLd9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d271eb-34d4-4715-b5b0-2494dd16589b_720x1830.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xLd9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d271eb-34d4-4715-b5b0-2494dd16589b_720x1830.png" width="250" height="635.4166666666666" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b8d271eb-34d4-4715-b5b0-2494dd16589b_720x1830.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1830,&quot;width&quot;:720,&quot;resizeWidth&quot;:250,&quot;bytes&quot;:183508,&quot;alt&quot;:&quot;Diagram of MVVM layers showing Orchestrator, Data Layer (Repositories), Model layer (DTO &amp; Payload, Module Type, Prop types), Application Layer (Hooks), and View Layer (Widgets).&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://techwood.substack.com/i/171272625?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d271eb-34d4-4715-b5b0-2494dd16589b_720x1830.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Diagram of MVVM layers showing Orchestrator, Data Layer (Repositories), Model layer (DTO &amp; Payload, Module Type, Prop types), Application Layer (Hooks), and View Layer (Widgets)." title="Diagram of MVVM layers showing Orchestrator, Data Layer (Repositories), Model layer (DTO &amp; Payload, Module Type, Prop types), Application Layer (Hooks), and View Layer (Widgets)." srcset="https://substackcdn.com/image/fetch/$s_!xLd9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d271eb-34d4-4715-b5b0-2494dd16589b_720x1830.png 424w, https://substackcdn.com/image/fetch/$s_!xLd9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d271eb-34d4-4715-b5b0-2494dd16589b_720x1830.png 848w, https://substackcdn.com/image/fetch/$s_!xLd9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d271eb-34d4-4715-b5b0-2494dd16589b_720x1830.png 1272w, https://substackcdn.com/image/fetch/$s_!xLd9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d271eb-34d4-4715-b5b0-2494dd16589b_720x1830.png 1456w" sizes="100vw"></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><figcaption class="image-caption"><em>View layer (highlighted) in the big picture of MVVM.</em></figcaption></figure></div><p>In this article, you&#8217;ll learn:</p><ol><li><p>How and where to keep the layout for the whole app in TanStack Router.</p></li><li><p>How and why keep your components clean.</p></li><li><p>How easy it is to test clean React components.</p></li></ol><p>Before we begin, I want to remind, that in this MVVM series, we&#8217;re building a country quiz app. The app prompts the user with the flag, and the user has to guess the country. Once they entered and sent the answer, we save it in localStorage. Then we display the answer history in the left sidebar. You can find <a href="https://github.com/ole-techwood/articles/tree/mvvm">the source code on GitHub</a> and the <a href="https://codesandbox.io/p/github/ole-techwood/articles/mvvm">Demo on CodeSandbox</a>.</p><h2>Shape of the whole app</h2><p><a href="https://tanstack.com/router/latest/docs/framework/react/routing/file-based-routing">TanStack Router recommends utilizing its file-based routing system</a>. That&#8217;s why when we bootstrapped our app in <a href="https://techwood.substack.com/p/react-mvvm-data-layer">the data layer article</a>, we got the <code>routes</code> folder with two files:</p><ul><li><p><code>__root.tsx</code> - wrapper for all routes in our app.</p></li><li><p><code>index.tsx</code> - main route in our app. It stands for the <code>/</code> path.</p></li></ul><p>At the moment, let&#8217;s focus our attention on <code>__root.tsx</code> , because that&#8217;s where we&#8217;ll keep the layout of the app:</p><pre><code>import { Grid } from "@mui/material";
import { Outlet, createRootRoute } from "@tanstack/react-router";

export const Route = createRootRoute({
  component: () =&gt; (
    &lt;Grid container sx={{ height: "100vh" }}&gt;
      &lt;Grid
        size={3}
        sx={{
          height: "100%",
          backgroundColor: "ButtonShadow",
          overflowY: "scroll",
        }}
      &gt;
        {/* Put here the history list once it's implemented */}
      &lt;/Grid&gt;
      &lt;Grid size={9}&gt;
        &lt;Outlet /&gt;
      &lt;/Grid&gt;
    &lt;/Grid&gt;
  ),
});</code></pre><p>Let&#8217;s break it down:</p><ol><li><p>First, we create a root route. <a href="https://tanstack.com/router/latest/docs/framework/react/api/router/createRootRouteFunction">The official documentation</a> explains it: &#8220;The <code>createRootRoute</code> function returns a new root route instance&#8221;. The difference between the root route and the index route is that the root route doesn&#8217;t have the path, like the index route does (<code>/</code> path). Root route is used to create a route tree and store the layout of the app.</p></li><li><p>Second, we define a structure of the layout using <code>Grid</code> component. This component comes from the <a href="https://mui.com/material-ui/getting-started/">MUI library</a>. Using it is an optional step. You can choose a different components library or skip any libraries entirely. I&#8217;ll continue utilizing MUI in all the next tutorials, because it&#8217;s just easier and quicker to develop UI with its wide range of components.</p><p>With the <code>Grid</code> component, we defined the next layout:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9IE-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a75d369-222a-4e5f-8dcc-70d403f7319f_3025x1664.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9IE-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a75d369-222a-4e5f-8dcc-70d403f7319f_3025x1664.png 424w, https://substackcdn.com/image/fetch/$s_!9IE-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a75d369-222a-4e5f-8dcc-70d403f7319f_3025x1664.png 848w, https://substackcdn.com/image/fetch/$s_!9IE-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a75d369-222a-4e5f-8dcc-70d403f7319f_3025x1664.png 1272w, https://substackcdn.com/image/fetch/$s_!9IE-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a75d369-222a-4e5f-8dcc-70d403f7319f_3025x1664.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9IE-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a75d369-222a-4e5f-8dcc-70d403f7319f_3025x1664.png" width="1456" height="801" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5a75d369-222a-4e5f-8dcc-70d403f7319f_3025x1664.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:801,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:247637,&quot;alt&quot;:&quot;Wireframe layout showing a left sidebar labeled &#8220;Sidebar with the history&#8221; and main area labeled &#8220;Main content with quiz.&#8221;&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;:&quot;https://techwood.substack.com/i/171272625?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a75d369-222a-4e5f-8dcc-70d403f7319f_3025x1664.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Wireframe layout showing a left sidebar labeled &#8220;Sidebar with the history&#8221; and main area labeled &#8220;Main content with quiz.&#8221;" title="Wireframe layout showing a left sidebar labeled &#8220;Sidebar with the history&#8221; and main area labeled &#8220;Main content with quiz.&#8221;" srcset="https://substackcdn.com/image/fetch/$s_!9IE-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a75d369-222a-4e5f-8dcc-70d403f7319f_3025x1664.png 424w, https://substackcdn.com/image/fetch/$s_!9IE-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a75d369-222a-4e5f-8dcc-70d403f7319f_3025x1664.png 848w, https://substackcdn.com/image/fetch/$s_!9IE-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a75d369-222a-4e5f-8dcc-70d403f7319f_3025x1664.png 1272w, https://substackcdn.com/image/fetch/$s_!9IE-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a75d369-222a-4e5f-8dcc-70d403f7319f_3025x1664.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><figcaption class="image-caption"><em>The layout of the app stored in the root route.</em></figcaption></figure></div><p>Humble, but more than enough for our simple app.</p></li><li><p>The final touch is <a href="https://tanstack.com/router/latest/docs/framework/react/guide/outlets">the Outlet component</a>, which is provided by TanStack Router. The <code>Outlet</code> renders the next potentially matching child route. If the user has entered <code>/</code> in the browser&#8217;s search bar, the <code>Outlet</code> will render the UI matching the <code>/</code> path.</p></li></ol><p>Good, we have the layout. Let&#8217;s fill it with the content now.</p><h2>Let the users guess the country</h2><p>Now that we&#8217;ve defined the layout, it&#8217;s time to focus on the <code>Quiz</code> widget that the user interacts with:</p><pre><code>// src/modules/Home/view/Quiz.tsx

import {
  Button,
  Card,
  CardActions,
  CardContent,
  CardMedia,
} from "@mui/material";
import type { QuizProps } from "../model";
import { QuizForm } from "./QuizForm";
import { QuizResult } from "./QuizResult";

export const Quiz: React.FC&lt;QuizProps&gt; = ({
  country,
  form,
  result,
  onSubmit,
}) =&gt; {
  return (
    &lt;Card sx={{ width: 320, margin: "0 auto" }}&gt;
      &lt;CardMedia
        sx={{ height: 198 }}
        image={country.flag}
        title="Country flag"
      /&gt;
      &lt;CardContent sx={(theme) =&gt; ({ padding: theme.spacing(2, 1) })}&gt;
        {!result ? &lt;QuizForm form={form} /&gt; : &lt;QuizResult result={result} /&gt;}
      &lt;/CardContent&gt;
      &lt;CardActions sx={{ display: "flex", justifyContent: "end" }}&gt;
        &lt;Button size="small" variant="contained" onClick={onSubmit}&gt;
          {!result ? "Submit answer" : "Next country"}
        &lt;/Button&gt;
      &lt;/CardActions&gt;
    &lt;/Card&gt;
  );
};</code></pre><blockquote><p><strong>NOTE:</strong> What&#8217;s the widget? - It is an individual component that renders the UI and responds to events. Why do I call it a widget? - It&#8217;s a Flutter-inspired name to call a React component, which already combines a couple of other smaller components. For example, the <code>Quiz</code> widget blends <code>Card</code>, <code>Button</code>, <code>CardContent</code>, and so on:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4HIn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4a983c6-980a-4a86-866b-76c3bc2cdc5a_2266x1658.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4HIn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4a983c6-980a-4a86-866b-76c3bc2cdc5a_2266x1658.png 424w, https://substackcdn.com/image/fetch/$s_!4HIn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4a983c6-980a-4a86-866b-76c3bc2cdc5a_2266x1658.png 848w, https://substackcdn.com/image/fetch/$s_!4HIn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4a983c6-980a-4a86-866b-76c3bc2cdc5a_2266x1658.png 1272w, https://substackcdn.com/image/fetch/$s_!4HIn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4a983c6-980a-4a86-866b-76c3bc2cdc5a_2266x1658.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4HIn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4a983c6-980a-4a86-866b-76c3bc2cdc5a_2266x1658.png" width="676" height="494.4642857142857" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a4a983c6-980a-4a86-866b-76c3bc2cdc5a_2266x1658.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1065,&quot;width&quot;:1456,&quot;resizeWidth&quot;:676,&quot;bytes&quot;:302552,&quot;alt&quot;:&quot;Diagram showing a quiz widget with China&#8217;s flag, an input field, and a submit button labeled as &#8220;Widget,&#8221; with each element (flag, input, button) marked as separate &#8220;Components.&#8221;&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;:&quot;https://techwood.substack.com/i/171272625?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4a983c6-980a-4a86-866b-76c3bc2cdc5a_2266x1658.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Diagram showing a quiz widget with China&#8217;s flag, an input field, and a submit button labeled as &#8220;Widget,&#8221; with each element (flag, input, button) marked as separate &#8220;Components.&#8221;" title="Diagram showing a quiz widget with China&#8217;s flag, an input field, and a submit button labeled as &#8220;Widget,&#8221; with each element (flag, input, button) marked as separate &#8220;Components.&#8221;" srcset="https://substackcdn.com/image/fetch/$s_!4HIn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4a983c6-980a-4a86-866b-76c3bc2cdc5a_2266x1658.png 424w, https://substackcdn.com/image/fetch/$s_!4HIn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4a983c6-980a-4a86-866b-76c3bc2cdc5a_2266x1658.png 848w, https://substackcdn.com/image/fetch/$s_!4HIn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4a983c6-980a-4a86-866b-76c3bc2cdc5a_2266x1658.png 1272w, https://substackcdn.com/image/fetch/$s_!4HIn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4a983c6-980a-4a86-866b-76c3bc2cdc5a_2266x1658.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><figcaption class="image-caption"><em>Widget blends smaller components.</em></figcaption></figure></div></blockquote><p>Let&#8217;s break the <code>Quiz</code> widget step by step, just like we did with the layout:</p><ol><li><p>We start by creating a functional React component typed with <code>QuizProps</code>, which accepts the following props:</p><ul><li><p><strong>country -</strong> the object of randomly selected country to guess.</p></li><li><p><strong>form -</strong> <a href="https://tanstack.com/form/latest/docs/overview">TanStack Form&#8217;s</a> instance. TanStack Form is a headless form state manager. It includes submission, validation, composition, and other great staff. We touch on some UI aspects of the library later in this article. We also discuss form handling in <a href="https://techwood.substack.com/p/react-mvvm-application-layer">the application layer article</a>.</p><p>Feel free to use any form management library if you&#8217;re not comfy with TanStack Form.</p></li><li><p><strong>result -</strong> the result of the user&#8217;s attempt to guess the country.</p></li><li><p><strong>onSubmit -</strong> a function that&#8217;s called when the user clicks the action button. It either submits the answer or triggers loading the next country, if the result is already shown.</p></li></ul><p>For now, <code>QuizProps</code> can look like this:</p><pre><code>// src/modules/Home/model/home.props.ts

import type { CreateHistoryRecordDTO } from "@/modules/History/model";

export type QuizProps = {
  country: any;
  form: any;
  result?: CreateHistoryRecordDTO;
  onSubmit: VoidFunction;
};</code></pre><p>We&#8217;ll fix it when we develop the hook to handle quiz logic and apply <a href="https://techwood.substack.com/i/171264907/prop-types-provide-data-or-configuration-to-ui">the trick</a>.</p></li><li><p>I use <a href="https://mui.com/material-ui/react-card/">Card MUI&#8217;s component</a> and its pieces to create an actual UI.</p></li><li><p>Pay attention to this line:</p><pre><code>{!result ? &lt;QuizForm form={form} /&gt; : &lt;QuizResult result={result} /&gt;}</code></pre><p>Here we have 2 additional components: <code>QuizForm</code> and <code>QuizResult</code>. I created them because of 2 reasons:</p><ol><li><p>Make <code>Quiz</code> widget smaller, by moving UI with different responsibilities to different widgets.</p></li><li><p>Fulfill the <a href="https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html">Single Responsibility principle of SOLID.</a></p></li></ol><p>Let&#8217;s take a look at each widget. &#128071;</p></li></ol><h3>Answering form</h3><p>Since the form is what the user sees first, let&#8217;s begin with <code>QuizForm</code>, the input widget responsible for capturing the answer:</p><pre><code>// src/modules/Home/view/QuizForm.tsx

import { TextField } from "@mui/material";
import type { QuizFormProps } from "../model";

export const QuizForm: React.FC&lt;QuizFormProps&gt; = ({ form }) =&gt; {
  return (
    &lt;form&gt;
      &lt;form.Field name="answer"&gt;
        {(field) =&gt; (
          &lt;TextField
            label="Your answer"
            variant="outlined"
            sx={{ width: "100%" }}
            error={!field.state.meta.isValid}
            helperText={field.state.meta.errors[0]}
            value={field.state.value}
            onChange={(event) =&gt; field.handleChange(event.target.value)}
          /&gt;
        )}
      &lt;/form.Field&gt;
    &lt;/form&gt;
  );
};</code></pre><p>Again, it&#8217;s a stateless, logic-free functional component which expects to receive 1 property: <code>form</code>. This <code>form </code>instance is the same one passed into the <code>Quiz</code> widget, so the <code>QuizFormProps</code> can look like this:</p><pre><code>export type QuizFormProps = Pick&lt;QuizProps, "form"&gt;;</code></pre><p>The next piece of code might be confusing to those unfamiliar with TanStack Form:</p><pre><code>//...
&lt;form&gt;
  &lt;form.Field name="answer"&gt;
//...</code></pre><p>For JSX, <code>form</code> is just a variable in props (not the HTML tag), so <a href="https://tanstack.com/form/latest/docs/framework/react/guides/basic-concepts#field">form.Field</a> works just like any namespace object. The outer <code>&lt;form&gt;</code> is a native HTML tag. JSX automatically distinguishes them, so no linter errors.</p><p>TanStack Form is a headless library, meaning we can integrate any input into it. This is what we did with <a href="https://mui.com/material-ui/react-text-field/">TextField</a> from MUI:</p><pre><code>{(field) =&gt; (
  &lt;TextField
    label="Your answer"
    variant="outlined"
    sx={{ width: "100%" }}
    error={!field.state.meta.isValid}
    helperText={field.state.meta.errors[0]}
&#9;  value={field.state.value}
&#9;  onChange={(event) =&gt; field.handleChange(event.target.value)}
  /&gt;
)}</code></pre><p>A function with a <code>field</code> property exposed by TanStack Form as well. I think the code is self-descriptive enough here, but to grasp all the details, read <a href="https://tanstack.com/form/latest/docs/framework/react/guides/basic-concepts#field">the official Field documentation.</a></p><p><code>QuizForm</code> is ready, great!</p><h3>Right or wrong?</h3><p><code>QuizResult</code> displays either a success or failure message based on whether the answer is correct:</p><pre><code>// src/modules/Home/view/QuizResult.tsx

import React from "react";
import type { QuizResultProps } from "../model";
import { Alert, Stack, Typography } from "@mui/material";

export const QuizResult: React.FC&lt;QuizResultProps&gt; = ({ result }) =&gt; {
  return (
    &lt;Stack&gt;
      {result?.countryName.toLowerCase() ===
      result?.userAnswer.toLowerCase() ? (
        &lt;React.Fragment&gt;
          &lt;Typography component="div" sx={{ mt: 2 }} color="success"&gt;
            Brilliant work!
          &lt;/Typography&gt;
          &lt;Alert variant="outlined" severity="success"&gt;
            {result?.countryName}
          &lt;/Alert&gt;
        &lt;/React.Fragment&gt;
      ) : (
        &lt;React.Fragment&gt;
          &lt;Typography color="warning"&gt;
            No worries. Learning is a process!
          &lt;/Typography&gt;
          &lt;Alert variant="outlined" severity="warning"&gt;
            {result?.userAnswer}
          &lt;/Alert&gt;
          &lt;Typography component="div" sx={{ mt: 2 }} color="success"&gt;
            Correct answer
          &lt;/Typography&gt;
          &lt;Alert variant="outlined" severity="success"&gt;
            {result?.countryName}
          &lt;/Alert&gt;
        &lt;/React.Fragment&gt;
      )}
    &lt;/Stack&gt;
  );
};</code></pre><p>It expects to receive a <code>result</code> as a single prop, which is the type of <code>CreateHistoryRecordDTO</code> from <a href="https://techwood.substack.com/i/171264907/dto-structuring-outgoing-data">the model layer article</a>:</p><pre><code>export type QuizResultProps = Pick&lt;QuizProps, "result"&gt;;</code></pre><p>Then <code>QuizResult</code> checks if the user&#8217;s answer is the same as an actual country name:</p><pre><code>result?.countryName.toLowerCase() === result?.userAnswer.toLowerCase()</code></pre><p>That&#8217;s how it decides whether to show &#8220;Brilliant work!&#8221; success message or &#8220;No worries. Learning is a process!&#8221; encouragement.</p><h3>Visual Recap</h3><p>Here&#8217;s a schema to reinforce how widgets are structured in the <code>Home</code> module:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LPRr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d71667c-c7e2-404f-bb78-8dd5a46223a8_867x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LPRr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d71667c-c7e2-404f-bb78-8dd5a46223a8_867x720.png 424w, https://substackcdn.com/image/fetch/$s_!LPRr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d71667c-c7e2-404f-bb78-8dd5a46223a8_867x720.png 848w, https://substackcdn.com/image/fetch/$s_!LPRr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d71667c-c7e2-404f-bb78-8dd5a46223a8_867x720.png 1272w, https://substackcdn.com/image/fetch/$s_!LPRr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d71667c-c7e2-404f-bb78-8dd5a46223a8_867x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LPRr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d71667c-c7e2-404f-bb78-8dd5a46223a8_867x720.png" width="304" height="252.4567474048443" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6d71667c-c7e2-404f-bb78-8dd5a46223a8_867x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:867,&quot;resizeWidth&quot;:304,&quot;bytes&quot;:78509,&quot;alt&quot;:&quot;Diagram of a Home module showing the View Layer. Inside it, a Quiz widget contains two components: QuizForm and QuizResult.&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;:&quot;https://techwood.substack.com/i/171272625?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d71667c-c7e2-404f-bb78-8dd5a46223a8_867x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Diagram of a Home module showing the View Layer. Inside it, a Quiz widget contains two components: QuizForm and QuizResult." title="Diagram of a Home module showing the View Layer. Inside it, a Quiz widget contains two components: QuizForm and QuizResult." srcset="https://substackcdn.com/image/fetch/$s_!LPRr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d71667c-c7e2-404f-bb78-8dd5a46223a8_867x720.png 424w, https://substackcdn.com/image/fetch/$s_!LPRr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d71667c-c7e2-404f-bb78-8dd5a46223a8_867x720.png 848w, https://substackcdn.com/image/fetch/$s_!LPRr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d71667c-c7e2-404f-bb78-8dd5a46223a8_867x720.png 1272w, https://substackcdn.com/image/fetch/$s_!LPRr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d71667c-c7e2-404f-bb78-8dd5a46223a8_867x720.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><figcaption class="image-caption"><em>Home&#8217;s view layer &#8594; Quiz &#8594; QuizForm (input) or QuizResult (output)</em></figcaption></figure></div><p>Because widgets are regular React components, they can contain other widgets. Of course, for better performance and maintainability, it&#8217;s better to keep nesting shallow.</p><h3>Folders organization</h3><p>This is how your folder structure should look when you finish creating the view layer of the <code>Home</code> module:</p><pre><code>src
&#9500;&#9472;&#9472; modules
&#9474;   &#9500;&#9472;&#9472; History
&#9474;   &#9492;&#9472;&#9472; Home
&#9474;       &#9500;&#9472;&#9472; data
&#9474;       &#9500;&#9472;&#9472; model
&#9474;       &#9492;&#9472;&#9472; view
&#9474;           &#9500;&#9472;&#9472; Quiz.tsx
&#9474;           &#9500;&#9472;&#9472; QuizForm.tsx
&#9474;           &#9500;&#9472;&#9472; QuizResult.tsx
&#9474;           &#9500;&#9472;&#9472; index.ts <em>- index file for re-exporting</em>
&#9500;&#9472;&#9472; routes
&#9474;   &#9500;&#9472;&#9472; __root.tsx
&#9474;   &#9492;&#9472;&#9472; index.tsx</code></pre><h2>Previewing the Quiz</h2><p>Now we can see the <code>Quiz</code> widget by displaying it on the main route. Open <code>src/routes/index.tsx</code> and write the next code:</p><pre><code>import { Quiz } from "@/modules/Home/view";
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/")({
  component: Home,
});

// Temporar orchestrating component
function Home() {
  const mockField = {
    state: {
      value: "",
      meta: {
        isValid: true,
        errors: [],
      },
    },
    handleChange: () =&gt; {},
  };

  const mockForm = {
    Field: ({ children }: any) =&gt; children(mockField),
  } as any;

  const mockProps = {
    country: { flag: "https://flagcdn.com/w320/cn.png", name: "China" },
    form: mockForm,
    result: undefined,
    onSubmit: () =&gt; {},
  };

  return &lt;Quiz {...mockProps} /&gt;;
}</code></pre><p>This isn&#8217;t the correct way of doing such things, because:</p><ol><li><p>We should not mix <a href="https://techwood.substack.com/p/react-mvvm-orchestrator">Orchestrator&#8217;s code</a> with the route&#8217;s code.</p></li><li><p>Mocking is, obviously, only for testing.</p></li></ol><p>We&#8217;ll fix everything step-by-step in the next articles by implementing a real form and submission handling. Also, we&#8217;ll randomly pick a country to guess and display the result.</p><p>Depending on the properties you provided, you must see the next outcomes:</p><ul><li><p>Start of the quiz:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4KpR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa070ea47-6588-4869-8119-752efbc5a89d_2266x1658.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4KpR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa070ea47-6588-4869-8119-752efbc5a89d_2266x1658.png 424w, https://substackcdn.com/image/fetch/$s_!4KpR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa070ea47-6588-4869-8119-752efbc5a89d_2266x1658.png 848w, https://substackcdn.com/image/fetch/$s_!4KpR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa070ea47-6588-4869-8119-752efbc5a89d_2266x1658.png 1272w, https://substackcdn.com/image/fetch/$s_!4KpR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa070ea47-6588-4869-8119-752efbc5a89d_2266x1658.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4KpR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa070ea47-6588-4869-8119-752efbc5a89d_2266x1658.png" width="1456" height="1065" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a070ea47-6588-4869-8119-752efbc5a89d_2266x1658.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1065,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:257328,&quot;alt&quot;:&quot;Quiz interface showing the flag of China with a text input field labeled &#8220;Your answer&#8221; and a blue &#8220;Submit Answer&#8221; button.&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;:&quot;https://techwood.substack.com/i/171272625?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa070ea47-6588-4869-8119-752efbc5a89d_2266x1658.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Quiz interface showing the flag of China with a text input field labeled &#8220;Your answer&#8221; and a blue &#8220;Submit Answer&#8221; button." title="Quiz interface showing the flag of China with a text input field labeled &#8220;Your answer&#8221; and a blue &#8220;Submit Answer&#8221; button." srcset="https://substackcdn.com/image/fetch/$s_!4KpR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa070ea47-6588-4869-8119-752efbc5a89d_2266x1658.png 424w, https://substackcdn.com/image/fetch/$s_!4KpR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa070ea47-6588-4869-8119-752efbc5a89d_2266x1658.png 848w, https://substackcdn.com/image/fetch/$s_!4KpR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa070ea47-6588-4869-8119-752efbc5a89d_2266x1658.png 1272w, https://substackcdn.com/image/fetch/$s_!4KpR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa070ea47-6588-4869-8119-752efbc5a89d_2266x1658.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><figcaption class="image-caption"><em>Quiz start screen.</em></figcaption></figure></div></li><li><p>User entered correct answer:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KxaH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe66d9176-05cf-4c62-b2f4-1700ff635fef_2265x1662.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KxaH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe66d9176-05cf-4c62-b2f4-1700ff635fef_2265x1662.png 424w, https://substackcdn.com/image/fetch/$s_!KxaH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe66d9176-05cf-4c62-b2f4-1700ff635fef_2265x1662.png 848w, https://substackcdn.com/image/fetch/$s_!KxaH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe66d9176-05cf-4c62-b2f4-1700ff635fef_2265x1662.png 1272w, https://substackcdn.com/image/fetch/$s_!KxaH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe66d9176-05cf-4c62-b2f4-1700ff635fef_2265x1662.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KxaH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe66d9176-05cf-4c62-b2f4-1700ff635fef_2265x1662.png" width="1456" height="1068" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e66d9176-05cf-4c62-b2f4-1700ff635fef_2265x1662.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1068,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:266728,&quot;alt&quot;:&quot;Quiz result showing the flag of China, message &#8220;Brilliant work!&#8221;, correct answer &#8220;China&#8221; with a checkmark, and a &#8220;Next Country&#8221; button.&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;:&quot;https://techwood.substack.com/i/171272625?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe66d9176-05cf-4c62-b2f4-1700ff635fef_2265x1662.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Quiz result showing the flag of China, message &#8220;Brilliant work!&#8221;, correct answer &#8220;China&#8221; with a checkmark, and a &#8220;Next Country&#8221; button." title="Quiz result showing the flag of China, message &#8220;Brilliant work!&#8221;, correct answer &#8220;China&#8221; with a checkmark, and a &#8220;Next Country&#8221; button." srcset="https://substackcdn.com/image/fetch/$s_!KxaH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe66d9176-05cf-4c62-b2f4-1700ff635fef_2265x1662.png 424w, https://substackcdn.com/image/fetch/$s_!KxaH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe66d9176-05cf-4c62-b2f4-1700ff635fef_2265x1662.png 848w, https://substackcdn.com/image/fetch/$s_!KxaH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe66d9176-05cf-4c62-b2f4-1700ff635fef_2265x1662.png 1272w, https://substackcdn.com/image/fetch/$s_!KxaH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe66d9176-05cf-4c62-b2f4-1700ff635fef_2265x1662.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><figcaption class="image-caption"><em>Correct answer screen.</em></figcaption></figure></div></li><li><p>User entered the wrong answer:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3SVS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedad2e08-bd63-44d5-9574-8794204bac30_2260x1653.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3SVS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedad2e08-bd63-44d5-9574-8794204bac30_2260x1653.png 424w, https://substackcdn.com/image/fetch/$s_!3SVS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedad2e08-bd63-44d5-9574-8794204bac30_2260x1653.png 848w, https://substackcdn.com/image/fetch/$s_!3SVS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedad2e08-bd63-44d5-9574-8794204bac30_2260x1653.png 1272w, https://substackcdn.com/image/fetch/$s_!3SVS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedad2e08-bd63-44d5-9574-8794204bac30_2260x1653.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3SVS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedad2e08-bd63-44d5-9574-8794204bac30_2260x1653.png" width="1456" height="1065" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/edad2e08-bd63-44d5-9574-8794204bac30_2260x1653.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1065,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:416433,&quot;alt&quot;:&quot;Quiz screen with Andorra&#8217;s flag. User answer: &#8220;I don&#8217;t know.&#8221; Message: &#8220;No worries. Learning is a process!&#8221; Correct answer: &#8220;Andorra.&#8221;&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;:&quot;https://techwood.substack.com/i/171272625?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedad2e08-bd63-44d5-9574-8794204bac30_2260x1653.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Quiz screen with Andorra&#8217;s flag. User answer: &#8220;I don&#8217;t know.&#8221; Message: &#8220;No worries. Learning is a process!&#8221; Correct answer: &#8220;Andorra.&#8221;" title="Quiz screen with Andorra&#8217;s flag. User answer: &#8220;I don&#8217;t know.&#8221; Message: &#8220;No worries. Learning is a process!&#8221; Correct answer: &#8220;Andorra.&#8221;" srcset="https://substackcdn.com/image/fetch/$s_!3SVS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedad2e08-bd63-44d5-9574-8794204bac30_2260x1653.png 424w, https://substackcdn.com/image/fetch/$s_!3SVS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedad2e08-bd63-44d5-9574-8794204bac30_2260x1653.png 848w, https://substackcdn.com/image/fetch/$s_!3SVS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedad2e08-bd63-44d5-9574-8794204bac30_2260x1653.png 1272w, https://substackcdn.com/image/fetch/$s_!3SVS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedad2e08-bd63-44d5-9574-8794204bac30_2260x1653.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><figcaption class="image-caption"><em>Wrong answer screen.</em></figcaption></figure></div></li></ul><h2>Testing</h2><blockquote><p><strong>NOTE:</strong> I&#8217;m using <a href="https://vitest.dev/">Vitest</a> and <a href="https://testing-library.com/docs/react-testing-library/intro/">React Testing Library</a> to write unit tests.</p></blockquote><h3>Test start of the quiz</h3><p>First, let&#8217;s test the <code>Quiz</code> widget:</p><pre><code>// src/modules/Home/view/tests/Quiz.test.tsx

import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { Quiz } from "../Quiz";
import type { QuizProps } from "../../model";

// Mock Material-UI components to avoid testing library complications
vi.mock("@mui/material", () =&gt; ({
  Button: ({ children, onClick, ...props }: any) =&gt; (
    &lt;button onClick={onClick} {...props}&gt;
      {children}
    &lt;/button&gt;
  ),
  Card: ({ children }: any) =&gt; &lt;div&gt;{children}&lt;/div&gt;,
  CardActions: ({ children }: any) =&gt; &lt;div&gt;{children}&lt;/div&gt;,
  CardContent: ({ children }: any) =&gt; &lt;div&gt;{children}&lt;/div&gt;,
  CardMedia: ({ title, image }: any) =&gt; &lt;img alt={title} src={image} /&gt;,
}));

// Mock child components
vi.mock("../QuizForm", () =&gt; ({
  QuizForm: ({ form }: any) =&gt; (
    &lt;div data-testid="quiz-form"&gt;
      QuizForm Component - Form State: {JSON.stringify(form)}
    &lt;/div&gt;
  ),
}));

vi.mock("../QuizResult", () =&gt; ({
  QuizResult: ({ result }: any) =&gt; (
    &lt;div data-testid="quiz-result"&gt;
      QuizResult Component - Result: {JSON.stringify(result)}
    &lt;/div&gt;
  ),
}));

describe("Quiz", () =&gt; {
  const mockOnSubmit = vi.fn();

  const mockCountry = {
    flag: "https://example.com/flag.png",
    name: "TestCountry",
  };

  const mockForm = {
    Field: ({ children }: any) =&gt;
      children({ state: { value: "", meta: { isValid: true, errors: [] } } }),
    handleSubmit: vi.fn(),
    resetField: vi.fn(),
  } as any;

  const mockResult = {
    flagImage: "https://example.com/flag.png",
    countryName: "TestCountry",
    userAnswer: "TestAnswer",
  };

  beforeEach(() =&gt; {
    vi.clearAllMocks();
  });

  afterEach(() =&gt; {
    vi.restoreAllMocks();
  });

  it("should render quiz form when no result is provided", () =&gt; {
    const props: QuizProps = {
      country: mockCountry,
      form: mockForm,
      result: undefined,
      onSubmit: mockOnSubmit,
    };

    render(&lt;Quiz {...props} /&gt;);

    expect(screen.getByTestId("quiz-form")).toBeDefined();
    expect(screen.queryByTestId("quiz-result")).toBeNull();
    expect(screen.getByText("Submit answer")).toBeDefined();
  });

  it("should render quiz result when result is provided", () =&gt; {
    const props: QuizProps = {
      country: mockCountry,
      form: mockForm,
      result: mockResult,
      onSubmit: mockOnSubmit,
    };

    render(&lt;Quiz {...props} /&gt;);

    expect(screen.getByTestId("quiz-result")).toBeDefined();
    expect(screen.queryByTestId("quiz-form")).toBeNull();
    expect(screen.getByText("Next country")).toBeDefined();
  });

// Find other test cases here: https://github.com/ole-techwood/articles/blob/mvvm/src/modules/Home/view/tests/Quiz.test.tsx
});</code></pre><p>Pay attention to how easy it is to test the widget when we don&#8217;t have any business logic inside. All we have to mock is the underlying components:</p><pre><code>// Mock Material-UI components to avoid testing library complications
vi.mock("@mui/material", () =&gt; ({
  Button: ({ children, onClick, ...props }: any) =&gt; (
    &lt;button onClick={onClick} {...props}&gt;
      {children}
    &lt;/button&gt;
  ),
  Card: ({ children }: any) =&gt; &lt;div&gt;{children}&lt;/div&gt;,
  CardActions: ({ children }: any) =&gt; &lt;div&gt;{children}&lt;/div&gt;,
  CardContent: ({ children }: any) =&gt; &lt;div&gt;{children}&lt;/div&gt;,
  CardMedia: ({ title, image }: any) =&gt; &lt;img alt={title} src={image} /&gt;,
}));

// Mock child components
vi.mock("../QuizForm", () =&gt; ({
  QuizForm: ({ form }: any) =&gt; (
    &lt;div data-testid="quiz-form"&gt;
      QuizForm Component - Form State: {JSON.stringify(form)}
    &lt;/div&gt;
  ),
}));

vi.mock("../QuizResult", () =&gt; ({
  QuizResult: ({ result }: any) =&gt; (
    &lt;div data-testid="quiz-result"&gt;
      QuizResult Component - Result: {JSON.stringify(result)}
    &lt;/div&gt;
  ),
}));</code></pre><p>We control the widget&#8217;s behavior in tests by passing different props. No need to mock hooks or data fetching logic:</p><pre><code>it("should render quiz form when no result is provided", () =&gt; {
  const props: QuizProps = {
    country: mockCountry,
    form: mockForm,
    result: undefined,
    onSubmit: mockOnSubmit,
  };

  render(&lt;Quiz {...props} /&gt;);

  expect(screen.getByTestId("quiz-form")).toBeDefined();
  expect(screen.queryByTestId("quiz-result")).toBeNull();
  expect(screen.getByText("Submit answer")).toBeDefined();
});

it("should render quiz result when result is provided", () =&gt; {
  const props: QuizProps = {
    country: mockCountry,
    form: mockForm,
    result: mockResult,
    onSubmit: mockOnSubmit,
  };

  render(&lt;Quiz {...props} /&gt;);

  expect(screen.getByTestId("quiz-result")).toBeDefined();
  expect(screen.queryByTestId("quiz-form")).toBeNull();
  expect(screen.getByText("Next country")).toBeDefined();
});</code></pre><h3>Test the answering form</h3><p>This is how we can test <code>QuizForm</code>:</p><pre><code>// src/modules/Home/view/tests/QuizForm.test.tsx

import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { QuizForm } from "../QuizForm";
import type { QuizFormProps } from "../../model";

// Mock Material-UI components
vi.mock("@mui/material", () =&gt; ({
  TextField: ({ label, value, onChange, error, helperText, ...props }: any) =&gt; (
    &lt;div&gt;
      &lt;label htmlFor="answer-input"&gt;{label}&lt;/label&gt;
      &lt;input
        id="answer-input"
        value={value}
        onChange={onChange}
        data-error={error}
        {...props}
      /&gt;
      {helperText &amp;&amp; &lt;span data-testid="helper-text"&gt;{helperText}&lt;/span&gt;}
    &lt;/div&gt;
  ),
}));

describe("QuizForm", () =&gt; {
  const mockHandleChange = vi.fn();

  const mockField = {
    state: {
      value: "",
      meta: {
        isValid: true,
        errors: [],
      },
    },
    handleChange: mockHandleChange,
  };

  const mockForm = {
    Field: ({ children }: any) =&gt; children(mockField),
  } as any;

  beforeEach(() =&gt; {
    vi.clearAllMocks();
  });

  afterEach(() =&gt; {
    vi.restoreAllMocks();
  });

  it("should render form with text input field", () =&gt; {
    const props: QuizFormProps = { form: mockForm };

    render(&lt;QuizForm {...props} /&gt;);

    expect(screen.getByLabelText("Your answer")).toBeDefined();
    expect(screen.getByRole("textbox")).toBeDefined();
  });
&#9;
// Find other test cases here: https://github.com/ole-techwood/articles/blob/mvvm/src/modules/Home/view/tests/QuizForm.test.tsx
});</code></pre><p>Again, dead simple, we mock the MUI&#8217;s <code>TextField</code> component and then check different conditions by passing different values in the <code>form</code> prop.</p><h3>Test results</h3><p>Last but not least - <code>QuizResult</code> tests:</p><pre><code>import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { QuizResult } from "../QuizResult";
import type { QuizResultProps } from "../../model";

// Mock Material-UI components
vi.mock("@mui/material", () =&gt; ({
  Stack: ({ children }: any) =&gt; &lt;div data-testid="stack"&gt;{children}&lt;/div&gt;,
  Typography: ({ children, color, ...props }: any) =&gt; (
    &lt;p data-color={color} {...props}&gt;
      {children}
    &lt;/p&gt;
  ),
  Alert: ({ children, severity, variant }: any) =&gt; (
    &lt;div data-testid="alert" data-severity={severity} data-variant={variant}&gt;
      {children}
    &lt;/div&gt;
  ),
}));

describe("QuizResult", () =&gt; {
  const mockCorrectResult = {
    flagImage: "https://example.com/flag.png",
    countryName: "France",
    userAnswer: "france",
  };

  beforeEach(() =&gt; {
    vi.clearAllMocks();
  });

  afterEach(() =&gt; {
    vi.restoreAllMocks();
  });

  it("should render success message for correct answer", () =&gt; {
    const props: QuizResultProps = { result: mockCorrectResult };

    render(&lt;QuizResult {...props} /&gt;);

    expect(screen.getByText("Brilliant work!")).toBeDefined();
    expect(screen.getByText("France")).toBeDefined();

    const successAlert = screen.getByTestId("alert");
    expect(successAlert.getAttribute("data-severity")).toBe("success");
  });

  // Find other test cases here: https://github.com/ole-techwood/articles/blob/mvvm/src/modules/Home/view/tests/QuizResult.test.tsx
});</code></pre><p>The testing approach is the same as with the previous two widgets; nothing new here.</p><p>And that&#8217;s it, we&#8217;re done with tests! Now, when you saw the whole process of creating a view layer with widgets, I want to give you a little homework.</p><h2>Study by practicing</h2><p>Don&#8217;t just grind tutorials. Better implement the new knowledge, practicing right off the bat! That&#8217;s why I want you to create a <code>HistoryList</code> widget within the <code>History</code> module of the app.</p><p>Requirements:</p><ul><li><p>It must be a clean React component with a single <code>history</code> property of the <code>HistoryRecord[]</code> type (we created the type in <a href="https://techwood.substack.com/i/171264907/module-type-visualize-model-layer">the model layer article</a>).</p></li><li><p>It must iterate through the <code>history</code> array and list all the answers.</p></li><li><p>It must be integrated into the left sidebar of the layout we created earlier in the article.</p></li><li><p>Add some tests for it.</p></li><li><p>Final result should look kinda 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_!aNR6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbc8c263-6f0a-474d-b2ff-cac0bfdac6d3_756x479.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aNR6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbc8c263-6f0a-474d-b2ff-cac0bfdac6d3_756x479.png 424w, https://substackcdn.com/image/fetch/$s_!aNR6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbc8c263-6f0a-474d-b2ff-cac0bfdac6d3_756x479.png 848w, https://substackcdn.com/image/fetch/$s_!aNR6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbc8c263-6f0a-474d-b2ff-cac0bfdac6d3_756x479.png 1272w, https://substackcdn.com/image/fetch/$s_!aNR6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbc8c263-6f0a-474d-b2ff-cac0bfdac6d3_756x479.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aNR6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbc8c263-6f0a-474d-b2ff-cac0bfdac6d3_756x479.png" width="756" height="479" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cbc8c263-6f0a-474d-b2ff-cac0bfdac6d3_756x479.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:479,&quot;width&quot;:756,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:43607,&quot;alt&quot;:&quot;Quiz history list showing two entries: wrong answer &#8220;I don&#8217;t know&#8221; for Andorra, and correct answer &#8220;China.&#8221;&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;:&quot;https://techwood.substack.com/i/171272625?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbc8c263-6f0a-474d-b2ff-cac0bfdac6d3_756x479.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Quiz history list showing two entries: wrong answer &#8220;I don&#8217;t know&#8221; for Andorra, and correct answer &#8220;China.&#8221;" title="Quiz history list showing two entries: wrong answer &#8220;I don&#8217;t know&#8221; for Andorra, and correct answer &#8220;China.&#8221;" srcset="https://substackcdn.com/image/fetch/$s_!aNR6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbc8c263-6f0a-474d-b2ff-cac0bfdac6d3_756x479.png 424w, https://substackcdn.com/image/fetch/$s_!aNR6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbc8c263-6f0a-474d-b2ff-cac0bfdac6d3_756x479.png 848w, https://substackcdn.com/image/fetch/$s_!aNR6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbc8c263-6f0a-474d-b2ff-cac0bfdac6d3_756x479.png 1272w, https://substackcdn.com/image/fetch/$s_!aNR6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcbc8c263-6f0a-474d-b2ff-cac0bfdac6d3_756x479.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><figcaption class="image-caption"><em>The final view of the history list that I implemented.</em></figcaption></figure></div></li></ul><p>Good luck! Keep in mind, you can always find the ready variant <a href="https://github.com/ole-techwood/articles/tree/mvvm/src/modules/History/view">in the source code</a>. But really, do your best and complete the task on your own (remember, AI is your friend, not an enemy! &#128578;).</p><h2>Conclusion</h2><p>This is it, folks! &#9989;</p><p>Today, we&#8217;ve discussed the view layer. In a nutshell, this is the layer our users see and interact with. So it&#8217;s essential to keep the layer and its parts clean, maintainable, and covered in tests.</p><p>Not just by reading, but by doing you:</p><ul><li><p>Learned what the widget is in MVVM architecture.</p></li><li><p>Got to know how to create a layout for the whole app in TanStack Router.</p></li><li><p>Understood how to create and test the widget.</p></li></ul><p>By this time, you have all the knowledge to create robust view layers utilizing MVVM architecture in production-grade systems!</p><h3>What&#8217;s next?</h3><p>Continue reading the next articles in the MVVM series:</p><ul><li><p><a href="https://techwood.substack.com/p/react-mvvm-application-layer">React MVVM Architecture with TanStack Router: Application Layer</a></p></li><li><p><a href="https://techwood.substack.com/p/react-mvvm-orchestrator">React MVVM Architecture with TanStack Router: Orchestrator</a></p></li></ul><p>Or go over the previous ones:</p><ul><li><p><a href="https://techwood.substack.com/p/react-mvvm-with-tanstack-router">React MVVM Architecture with TanStack Router: An Introduction</a></p></li><li><p><a href="https://techwood.substack.com/p/react-mvvm-data-layer">React MVVM Architecture with TanStack Router: Data Layer</a></p></li><li><p><a href="https://techwood.substack.com/p/react-mvvm-model-layer">React MVVM Architecture with TanStack Router: Model Layer</a></p></li></ul><div><hr></div><p>Creating content like this is not an easy ride, so I hope you&#8217;ll hit that like button and drop a comment. As usual, constructive feedback is always welcome!</p><p>&#128279; <a href="https://www.linkedin.com/in/olexandr-pylypyshyn/">Follow me on LinkedIn</a></p><p>&#128240; Read me on the platform of your choice: <a href="https://techwood.substack.com/">Substack</a>, <a href="https://dev.to/techwood">DEV.to</a></p><p>See ya soon &#128075;</p>]]></content:encoded></item><item><title><![CDATA[React MVVM Architecture with TanStack Router: Model Layer]]></title><description><![CDATA[Learn how to structure types in React apps using MVVM and TanStack Router. Organize DTOs, Payloads, and module types for scalable, type-safe architecture.]]></description><link>https://techwood.substack.com/p/react-mvvm-model-layer</link><guid isPermaLink="false">https://techwood.substack.com/p/react-mvvm-model-layer</guid><dc:creator><![CDATA[Olexandr]]></dc:creator><pubDate>Mon, 18 Aug 2025 12:29:40 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!PWJh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febf67089-b8d3-4e8c-8c42-939d67ab2811_1920x1280.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PWJh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febf67089-b8d3-4e8c-8c42-939d67ab2811_1920x1280.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PWJh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febf67089-b8d3-4e8c-8c42-939d67ab2811_1920x1280.jpeg 424w, https://substackcdn.com/image/fetch/$s_!PWJh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febf67089-b8d3-4e8c-8c42-939d67ab2811_1920x1280.jpeg 848w, https://substackcdn.com/image/fetch/$s_!PWJh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febf67089-b8d3-4e8c-8c42-939d67ab2811_1920x1280.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!PWJh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febf67089-b8d3-4e8c-8c42-939d67ab2811_1920x1280.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PWJh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febf67089-b8d3-4e8c-8c42-939d67ab2811_1920x1280.jpeg" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ebf67089-b8d3-4e8c-8c42-939d67ab2811_1920x1280.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:306049,&quot;alt&quot;:&quot;Close-up photograph of overlapping autumn leaves in shades of green, yellow, brown, and orange, showing detailed textures.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://techwood.substack.com/i/171264907?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febf67089-b8d3-4e8c-8c42-939d67ab2811_1920x1280.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Close-up photograph of overlapping autumn leaves in shades of green, yellow, brown, and orange, showing detailed textures." title="Close-up photograph of overlapping autumn leaves in shades of green, yellow, brown, and orange, showing detailed textures." srcset="https://substackcdn.com/image/fetch/$s_!PWJh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febf67089-b8d3-4e8c-8c42-939d67ab2811_1920x1280.jpeg 424w, https://substackcdn.com/image/fetch/$s_!PWJh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febf67089-b8d3-4e8c-8c42-939d67ab2811_1920x1280.jpeg 848w, https://substackcdn.com/image/fetch/$s_!PWJh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febf67089-b8d3-4e8c-8c42-939d67ab2811_1920x1280.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!PWJh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febf67089-b8d3-4e8c-8c42-939d67ab2811_1920x1280.jpeg 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><figcaption class="image-caption">Model layer. Photo by <a href="https://unsplash.com/@hasanalmasi?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Hasan Almasi</a> on <a href="https://unsplash.com/photos/close-up-photography-of-assorted-color-leaves-SpCrTUG2nu8?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></figcaption></figure></div><p>Types are playing an essential part in modern React applications. They&#8217;re the foundation for typing component props, shaping unstructured data like JSON, and defining request bodies. And that&#8217;s only the tip of the iceberg of use cases you might run into.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PtRd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ac59dd-e3d4-4862-a2e4-e15f04684953_600x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PtRd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ac59dd-e3d4-4862-a2e4-e15f04684953_600x720.png 424w, https://substackcdn.com/image/fetch/$s_!PtRd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ac59dd-e3d4-4862-a2e4-e15f04684953_600x720.png 848w, https://substackcdn.com/image/fetch/$s_!PtRd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ac59dd-e3d4-4862-a2e4-e15f04684953_600x720.png 1272w, https://substackcdn.com/image/fetch/$s_!PtRd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ac59dd-e3d4-4862-a2e4-e15f04684953_600x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PtRd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ac59dd-e3d4-4862-a2e4-e15f04684953_600x720.png" width="352" height="422.4" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/52ac59dd-e3d4-4862-a2e4-e15f04684953_600x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:600,&quot;resizeWidth&quot;:352,&quot;bytes&quot;:680636,&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;:false,&quot;internalRedirect&quot;:&quot;https://techwood.substack.com/i/171264907?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ac59dd-e3d4-4862-a2e4-e15f04684953_600x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!PtRd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ac59dd-e3d4-4862-a2e4-e15f04684953_600x720.png 424w, https://substackcdn.com/image/fetch/$s_!PtRd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ac59dd-e3d4-4862-a2e4-e15f04684953_600x720.png 848w, https://substackcdn.com/image/fetch/$s_!PtRd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ac59dd-e3d4-4862-a2e4-e15f04684953_600x720.png 1272w, https://substackcdn.com/image/fetch/$s_!PtRd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ac59dd-e3d4-4862-a2e4-e15f04684953_600x720.png 1456w" sizes="100vw"></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><figcaption class="image-caption"><em>Powerful, types are. Know how to handle them, you must or chaos they bring.</em></figcaption></figure></div><p>As developers, we often focus so much on business logic, performance, security, and other critical aspects of our apps that we forget about the type system. As the project evolves, types get messy - names are inconsistent, and there&#8217;s no clear place to store them. That hurts the developer experience. Teams start using <code>any</code> more commonly, and it leads to inconsistency between the backend and the frontend. As a result, bugs emerge.</p><p>Today we&#8217;re talking about the model layer, which is a part of <a href="https://open.substack.com/pub/techwood/p/react-mvvm-with-tanstack-router">the five-layer MVVM architecture</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_!rm2T!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03a9b90-fcbc-4b76-b158-7a46bf624958_720x1830.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rm2T!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03a9b90-fcbc-4b76-b158-7a46bf624958_720x1830.png 424w, https://substackcdn.com/image/fetch/$s_!rm2T!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03a9b90-fcbc-4b76-b158-7a46bf624958_720x1830.png 848w, https://substackcdn.com/image/fetch/$s_!rm2T!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03a9b90-fcbc-4b76-b158-7a46bf624958_720x1830.png 1272w, https://substackcdn.com/image/fetch/$s_!rm2T!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03a9b90-fcbc-4b76-b158-7a46bf624958_720x1830.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rm2T!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03a9b90-fcbc-4b76-b158-7a46bf624958_720x1830.png" width="250" height="635.4166666666666" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a03a9b90-fcbc-4b76-b158-7a46bf624958_720x1830.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1830,&quot;width&quot;:720,&quot;resizeWidth&quot;:250,&quot;bytes&quot;:187665,&quot;alt&quot;:&quot;Diagram of MVVM architecture layers: Orchestrator, Data Layer (Repositories), Model layer (highlighted) (DTO &amp; Payload, Module Type, Prop types), Application Layer (Hooks), and View Layer (Widgets).&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://techwood.substack.com/i/171264907?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03a9b90-fcbc-4b76-b158-7a46bf624958_720x1830.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Diagram of MVVM architecture layers: Orchestrator, Data Layer (Repositories), Model layer (highlighted) (DTO &amp; Payload, Module Type, Prop types), Application Layer (Hooks), and View Layer (Widgets)." title="Diagram of MVVM architecture layers: Orchestrator, Data Layer (Repositories), Model layer (highlighted) (DTO &amp; Payload, Module Type, Prop types), Application Layer (Hooks), and View Layer (Widgets)." srcset="https://substackcdn.com/image/fetch/$s_!rm2T!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03a9b90-fcbc-4b76-b158-7a46bf624958_720x1830.png 424w, https://substackcdn.com/image/fetch/$s_!rm2T!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03a9b90-fcbc-4b76-b158-7a46bf624958_720x1830.png 848w, https://substackcdn.com/image/fetch/$s_!rm2T!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03a9b90-fcbc-4b76-b158-7a46bf624958_720x1830.png 1272w, https://substackcdn.com/image/fetch/$s_!rm2T!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa03a9b90-fcbc-4b76-b158-7a46bf624958_720x1830.png 1456w" sizes="100vw"></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><figcaption class="image-caption"><em>Model layer (highlighted) in the big picture of MVVM.</em></figcaption></figure></div><p>Its goal is to tackle all of the hurdles listed above, which means fewer bugs, zero <code>any</code> usage (unless it&#8217;s absolutely required) and easier refactoring.</p><blockquote><p><strong>TIP:</strong> If you haven&#8217;t read <a href="https://open.substack.com/pub/techwood/p/react-mvvm-with-tanstack-router">my MVVM Introduction</a> yet, I highly recommend doing that first. It&#8217;ll help you understand where the model layer fits in the bigger picture.</p></blockquote><p>In this article, you&#8217;ll learn:</p><ul><li><p>How to add types to the unstructured data you receive and send to the external sources.</p></li><li><p>How to structure the data so it can be conveniently displayed on the UI.</p></li><li><p>How to organize the prop types in the MVVM style.</p></li></ul><p>Before we begin, I just want to remind, that in this MVVM series, we&#8217;re building a country quiz app. The app prompts the user with the flag, and the user has to guess the name of the country. Once they entered and sent the answer, we save it in the localStorage. Then we display the answer history in the left sidebar. You can find <a href="https://github.com/ole-techwood/articles/tree/mvvm">the source code on GitHub</a> and the <a href="https://codesandbox.io/p/github/ole-techwood/articles/mvvm">Demo on CodeSandbox</a>.</p><h2>Payload: Typing the API Response</h2><p>Payloads define a structure for the data your app <em>receives</em> from the external data source. For example, it can be a type of <code>GET</code> query or WebSocket channel response.</p><p>In <a href="https://open.substack.com/pub/techwood/p/react-mvvm-data-layer?r=5uvuls&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=false">the article explaining the data layer</a>, we had a method called <code>findCountries</code> to fetch all the countries. If you remember, we&#8217;ve put <code>any[]</code> as the response type instead of a valid one. Now let&#8217;s create a payload for it:</p><pre><code>// src/modules/Home/model/home.dto.ts

import * as z from "zod/v4";

// Define an object for a single country returned from the countries API
const countryPayload = z.object({
  flags: z.object({
    png: z.string().nonempty(),
    svg: z.string().nonempty(),
    alt: z.string(),
  }),
  name: z.object({
    common: z.string().nonempty(),
    official: z.string().nonempty(),
    nativeName: z.record(
      z.string().nonempty(),
      z.object({
        official: z.string().nonempty(),
        common: z.string().nonempty(),
      })
    ),
  }),
});

// Define an array of countries
export const countriesPayload = z.array(countryPayload);

// Define a type out of countryPayload object using Zod's infer
export type CountryPayload = z.infer&lt;typeof countryPayload&gt;;</code></pre><blockquote><p><strong>NOTE:</strong> Follow the link to get <a href="https://restcountries.com/v3.1/all?fields=name,flags">the structure for the payload</a></p></blockquote><p>To define a payload object and a type, I use <a href="https://zod.dev/v4">Zod</a>. It allows us to perform runtime validation of the layer&#8217;s entities. This ensures malformed data from the third-party APIs never breaks the app. Later, when we work on the business logic, I will show you detailed validation examples.</p><p>Currently, we&#8217;re interested in <code>CountryPayload</code> type, without Zod it would look like this:</p><pre><code>export type CountryPayload = {
  flags: {
    png: string;
    svg: string;
    alt: string;
  };
  name: {
    common: string;
    official: string;
    nativeName: {
      [key: string]: {
        official: string;
        common: string;
      };
    };
  };
};</code></pre><p>With the type created, your folder structure should look like this:</p><pre><code>src/
&#9500;&#9472;&#9472; modules/
&#9474;   &#9492;&#9472;&#9472; Home/
&#9474;       &#9492;&#9472;&#9472; model/
&#9474;           &#9500;&#9472;&#9472; home.dto.ts
&#9474;           &#9492;&#9472;&#9472; index.ts <em>- index file for re-exporting</em></code></pre><p>Yes, it&#8217;s fine to store payload in the DTO file.</p><p>Zod is a completely optional package, and its usage depends on your project&#8217;s needs. Like I said, I&#8217;ll show you more Zod benefits later in <a href="https://techwood.substack.com/p/react-mvvm-application-layer">the application layer article</a>. Meanwhile, you can specify a return type for the <code>findCountries</code> like this:</p><pre><code>findCountries() {
  return this.apiService.get&lt;CountryPayload[]&gt;("/all?fields=name,flags");
}</code></pre><h2>DTO: Structuring Outgoing Data</h2><p>DTOs, on the other hand, define a structure for the data you <em>transfer (meaning send)</em> to the external source. It can be a type for <code>POST</code> request&#8217;s body or for an object you save into <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">IndexedDB</a>. DTO can also represent query params for <code>GET</code> request.</p><p>In <code>HistoryRepository</code> we have a method <code>saveAnswer</code>:</p><pre><code>saveAnswer(createHistoryRecordDto: any) {
  return this.slice
    .getState()
    .saveAnswer({ ...createHistoryRecordDto, createdAt: Date.now() });
}</code></pre><p>Let&#8217;s define a DTO for <code>createHistoryRecordDto</code> param:</p><pre><code>// src/modules/History/model/history.dto.ts

import { z } from "zod";

export const createHistoryRecordDTO = z.object({
  flagImage: z.string().nonempty(),
  countryName: z.string().nonempty(),
  userAnswer: z.string().nonempty(),
});

export type CreateHistoryRecordDTO = z.infer&lt;typeof createHistoryRecordDTO&gt;;</code></pre><p>Put this DTO inside the <code>model</code> folder of the <code>History</code> module, just like we did with <code>CountryPayload</code> in the previous section.</p><p>Equivalent without Zod:</p><pre><code>export type CreateHistoryRecordDTO = {
  flagImage: string;
  countryName: string;
  userAnswer: string;
};</code></pre><p>This DTO consists of 3 properties:</p><ol><li><p><strong>flagImage</strong> - URL of the flag picture, so we can display the flag near each answer.</p></li><li><p><strong>countryName</strong> - correct name of the country.</p></li><li><p><strong>userAnswer</strong> - name of the country the user entered.</p></li></ol><p>Now we can specify a DTO type for the <code>saveAnswer</code> method like this:</p><pre><code>saveAnswer(createHistoryRecordDto: CreateHistoryRecordDTO) {
  return this.slice
    .getState()
    .saveAnswer({ ...createHistoryRecordDto, createdAt: Date.now() });
}</code></pre><p>With the DTO in place, we can send the data out safely and predictably. Now let&#8217;s look at how we shape the data <em>inside the app</em> using Module Types.</p><h2>Module Type: Visualize Model Layer</h2><p>This is the main type of the whole module. The module type holds properties to be shown on the UI. It doesn&#8217;t necessarily mean that <em>all</em> the properties from this type will be represented in the interface. Some properties exist for the business logic only, some can just sit there and patiently wait to be used at some point later.</p><p>Worth mentioning that sometimes module type can replace the payload, if the backend response is structured for the UI or business logic right when we receive it. If the data needs reshaping or enriching, keep the module type and the payload separate.</p><p>For example, <code>HistoryRecord</code> - the module type for the <code>History</code> module looks like this:</p><pre><code>// src/modules/History/model/history.ts

import { z } from "zod";
import { createHistoryRecordDTO } from "./history.dto";

// https://zod.dev/api#extend
export const historyRecord = createHistoryRecordDTO.extend({
  createdAt: z.number(),
});

// Convert historyRecord schema into Zod array, so we can validate the response from localStorage inside the application layer
export const historyRecords = z.array(historyRecord);

export type HistoryRecord = z.infer&lt;typeof historyRecord&gt;;</code></pre><p><code>historyRecord</code> schema inherits all the properties from <code>createHistoryRecordDTO</code>. It also adds one more <code>createdAt</code> timestamp indicating when the history record was created. <code>HistoryRecord</code> type infer the structure from the schema, as usual.</p><blockquote><p><strong>NOTE:</strong> <a href="https://zod.dev/api#extend">Learn more about extend function.</a></p></blockquote><p>Equivalent without Zod:</p><pre><code>export type HistoryRecord = CreateHistoryRecordDTO &amp; {
  createdAt: number;
}</code></pre><p>Use <code>HistoryRecord</code> inside <code>historySlice</code> to specify the type for the history:</p><pre><code>import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import type { HistoryRecord } from "../model";

export type HistorySlice = {
  history: HistoryRecord[]; // We add type here
  saveAnswer: (result: HistoryRecord) =&gt; void; // We add type here
  clearHistory: () =&gt; void;
};

export const historySlice = create&lt;HistorySlice&gt;()(
  // History slice implementation
);</code></pre><blockquote><p><strong>NOTE:</strong> We created <code>historySlice</code> in <a href="https://open.substack.com/pub/techwood/p/react-mvvm-data-layer?r=5uvuls&amp;utm_campaign=post&amp;utm_medium=web">the data layer article</a>.</p></blockquote><p>This is how <code>HistoryRecord</code> module type can be shown on the UI:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!op0l!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7732579f-3857-4e33-b949-bd275b18b373_6009x1269.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!op0l!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7732579f-3857-4e33-b949-bd275b18b373_6009x1269.png 424w, https://substackcdn.com/image/fetch/$s_!op0l!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7732579f-3857-4e33-b949-bd275b18b373_6009x1269.png 848w, https://substackcdn.com/image/fetch/$s_!op0l!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7732579f-3857-4e33-b949-bd275b18b373_6009x1269.png 1272w, https://substackcdn.com/image/fetch/$s_!op0l!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7732579f-3857-4e33-b949-bd275b18b373_6009x1269.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!op0l!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7732579f-3857-4e33-b949-bd275b18b373_6009x1269.png" width="1456" height="307" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7732579f-3857-4e33-b949-bd275b18b373_6009x1269.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:307,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2296357,&quot;alt&quot;:&quot;Diagram showing a TypeScript type HistoryRecord with fields for flagImage, countryName, userAnswer, and createdAt, mapped to a UI card displaying Nepal&#8217;s flag, country name, and a green check mark.&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;:&quot;https://techwood.substack.com/i/171264907?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7732579f-3857-4e33-b949-bd275b18b373_6009x1269.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Diagram showing a TypeScript type HistoryRecord with fields for flagImage, countryName, userAnswer, and createdAt, mapped to a UI card displaying Nepal&#8217;s flag, country name, and a green check mark." title="Diagram showing a TypeScript type HistoryRecord with fields for flagImage, countryName, userAnswer, and createdAt, mapped to a UI card displaying Nepal&#8217;s flag, country name, and a green check mark." srcset="https://substackcdn.com/image/fetch/$s_!op0l!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7732579f-3857-4e33-b949-bd275b18b373_6009x1269.png 424w, https://substackcdn.com/image/fetch/$s_!op0l!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7732579f-3857-4e33-b949-bd275b18b373_6009x1269.png 848w, https://substackcdn.com/image/fetch/$s_!op0l!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7732579f-3857-4e33-b949-bd275b18b373_6009x1269.png 1272w, https://substackcdn.com/image/fetch/$s_!op0l!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7732579f-3857-4e33-b949-bd275b18b373_6009x1269.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption"><em>Module type in UI representation.</em></figcaption></figure></div><h2>Prop types: Provide Data or Configuration to UI</h2><p>These are just regular prop types you would specify for the components in your React app. For example, we have a sidebar with a history list widget. This widget should accept the following properties:</p><ul><li><p><strong>history</strong> - an array with all history records to display.</p></li><li><p><strong>loading</strong> - a boolean value indicating whether we&#8217;re fetching the data at the moment.</p></li><li><p><strong>error</strong> - string holding the text of the error, if any occurred.</p></li></ul><p>Humbly, but enough for educational purposes. So the props type will look like this:</p><pre><code>export type HistoryListProps = {
  history: HistoryRecord[]; // an array of module types
  loading: boolean;
  error: string;
}</code></pre><blockquote><p><strong>NOTE:</strong> I don&#8217;t use Zod for prop types, because this is an overkill. But, of course, you can do it, if you have a strong reason.</p></blockquote><p>Then we provide it to the component through <code>React.FC</code>:</p><p>Easy, isn&#8217;t it? If you read <a href="https://open.substack.com/pub/techwood/p/react-mvvm-with-tanstack-router?r=5uvuls&amp;utm_campaign=post&amp;utm_medium=web">my MVVM intro</a>, you might remember I said this in the &#8220;Application Layer&#8221; section: &#8220;usually, each hook corresponds to a specific widget from the View layer&#8221;. Suppose we have a custom hook called useHistoryList. It fetches the history records from localStorage, handles loading and errors:</p><pre><code>export const useHistoryList = () =&gt; {
  // Hook implementation

  return { history, loading, error };
};</code></pre><p>As you can see, it returns the same set of properties that we need inside the <code>HistoryList</code> widget. So we can do a little trick with our <code>HistoryListProps</code>. Instead of specifying properties manually inside the type, we can infer them from the returning object of the <code>useHistoryList</code> hook:</p><pre><code>export type HistoryListProps = ReturnType&lt;typeof useHistoryList&gt;;</code></pre><blockquote><p><strong>NOTE:</strong> In the code example above we&#8217;ve used <a href="https://www.typescriptlang.org/docs/handbook/utility-types.html#returntypetype">TypeScript&#8217;s utility called ReturnType.</a></p></blockquote><p>That one-liner isn&#8217;t just beautiful - it automatically picks up new properties from the custom hook and adds each one fully typed to your props object.</p><p>A key downside to keep in mind is that if you change the return signature of the custom hook, <strong>it will break the type</strong> and <strong>cause TypeScript linter errors</strong>. But even with this tradeoff, the object returned by the custom hook and props type will be synced; the only place to refactor is the widget.</p><blockquote><p><strong>TIP:</strong> Remember to use utils like <a href="https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys">Omit</a> or <a href="https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys">Pick</a> to avoid silently carry over properties not meant for the UI.</p></blockquote><p>What&#8217;s that? What if you need an additional property that the custom hook doesn&#8217;t return? I&#8217;ve got you covered:</p><pre><code>export type HistoryListProps = ReturnType&lt;typeof useHistoryList&gt; &amp; {
  customProp: string;
};</code></pre><p>Even with using the trick, we can still add custom properties, thanks to robust TypeScript&#8217;s typing system.</p><blockquote><p><strong>NOTE: </strong>If you&#8217;re thinking &#8220;widgets, hooks, application layer&#8230; wtf&#129327;,&#8221; don&#8217;t worry. You&#8217;ll get it all if you read <a href="https://open.substack.com/pub/techwood/p/react-mvvm-with-tanstack-router?r=5uvuls&amp;utm_campaign=post&amp;utm_medium=web">the MVVM intro</a> and 2 articles about <a href="https://techwood.substack.com/p/react-mvvm-application-layer">application</a> and <a href="https://techwood.substack.com/p/react-mvvm-view-layer">view</a> layers. </p><p>We had to briefly look ahead here, because it&#8217;s impossible to explain prop types in isolation without referencing the layers that use them.</p></blockquote><h2>Testing</h2><blockquote><p><strong>NOTE:</strong> To write unit tests I&#8217;m using <a href="https://vitest.dev/">Vitest</a>.</p></blockquote><p>One of the reasons I tend to use Zod is that you can test model layer entities we&#8217;ve just discussed. This catches errors that TypeScript can&#8217;t, like empty strings or missing fields, which often cause runtime bugs in real-world apps. Here&#8217;s how to verify whether our DTO is correct:</p><pre><code>// src/modules/History/model/tests/history.dto.test.ts

import { describe, it, expect } from "vitest";
import { createHistoryRecordDTO } from "../history.dto";

describe("createHistoryRecordDTO", () =&gt; {
  it("should parse valid input", () =&gt; {
    const valid = {
      flagImage: "flag.png",
      countryName: "Ukraine",
      userAnswer: "Ukraine",
    };

    expect(createHistoryRecordDTO.parse(valid)).toEqual(valid);
  });

  it("should fail for empty strings", () =&gt; {
    const edge = {
      flagImage: "",
      countryName: "",
      userAnswer: "",
    };

    expect(() =&gt; createHistoryRecordDTO.parse(edge)).toThrow();
  });

  it("should fail for missing fields", () =&gt; {
    expect(() =&gt; createHistoryRecordDTO.parse({})).toThrow();
    expect(() =&gt;
      createHistoryRecordDTO.parse({ flagImage: "f.png" })
    ).toThrow();
  });

  it("should fail for wrong types", () =&gt; {
    expect(() =&gt;
      createHistoryRecordDTO.parse({
        flagImage: 123,
        countryName: true,
        userAnswer: null,
      })
    ).toThrow();
  });
});</code></pre><p>A piece of cake! No mocks, no spies, nothing. We just use regular Zod&#8217;s syntax, which we&#8217;d use in the production code.</p><p>One more test to verify <code>countryPayload</code>:</p><pre><code>// src/modules/Home/model/tests/home.dto.test.ts

import { describe, it, expect } from "vitest";
import { countriesPayload } from "../home.dto";

// Import the single country schema
const countryPayload = countriesPayload.element;

describe("countryPayload", () =&gt; {
  it("should parse valid input", () =&gt; {
    const valid = {
      flags: {
        png: "flag.png",
        svg: "flag.svg",
        alt: "Flag of Ukraine",
      },
      name: {
        common: "Ukraine",
        official: "Ukraine",
        nativeName: {
          ukr: { official: "&#1059;&#1082;&#1088;&#1072;&#1111;&#1085;&#1072;", common: "&#1059;&#1082;&#1088;&#1072;&#1111;&#1085;&#1072;" },
        },
      },
    };

    expect(countryPayload.parse(valid)).toEqual(valid);
  });

  it("should fail for empty strings in required fields", () =&gt; {
    const invalid = {
      flags: {
        png: "",
        svg: "",
        alt: "",
      },
      name: {
        common: "",
        official: "",
        nativeName: {
          "": { official: "", common: "" },
        },
      },
    };

    expect(() =&gt; countryPayload.parse(invalid)).toThrow();
  });

  it("should fail for missing fields", () =&gt; {
    expect(() =&gt; countryPayload.parse({})).toThrow();
    expect(() =&gt;
      countryPayload.parse({ flags: { png: "a", svg: "b", alt: "c" } })
    ).toThrow();
  });

  it("should fail for wrong types", () =&gt; {
    expect(() =&gt;
      countryPayload.parse({
        flags: "not-an-object",
        name: 123,
      })
    ).toThrow();
    expect(() =&gt;
      countryPayload.parse({
        flags: { png: 1, svg: 2, alt: 3 },
        name: { common: [], official: {}, nativeName: null },
      })
    ).toThrow();
  });
});</code></pre><p>In both cases, we test for:</p><ul><li><p>Valid input</p></li><li><p>Empty strings</p></li><li><p>Missing fields</p></li><li><p>Incorrect types</p></li></ul><p>Now that you&#8217;ve seen testing examples for both DTO and payload, here&#8217;s a small challenge to test your understanding.</p><h2>A little homework</h2><p>Try to add other tests for other model layer entities using the example that I&#8217;ve provided. Of course, you will find all the tests in <a href="https://github.com/ole-techwood/articles/tree/mvvm">the source code</a> if you get stuck. But I really encourage you to try on your own, because we learn the best when we take action!</p><h2>Conclusion</h2><p>If there&#8217;s one thing to take away from this article, it&#8217;s this: organize your types and take care of them. It&#8217;s a common place where things often get messy. Today, we&#8217;ve looked at the model layer, its entities, and how they help you organize your types and structure your code. You&#8217;ve learned:</p><ul><li><p>How to type your query bodies and responses utilizing DTOs and Payloads.</p></li><li><p>Why we need a Module Type and how it&#8217;s used across the module.</p></li></ul><p>You&#8217;ve also:</p><ul><li><p>Revised your knowledge about prop types and how MVVM&#8217;s hook-UI relation simplifies their maintenance.</p></li><li><p>Got to know how we can test the model layer entities, so no invalid data slips into your app, causing unexpected bugs.</p></li></ul><p>Now you have everything you need to build a robust model layer and keep your types consistent and predictable throughout the system. Additionally, Zod usage will make sure the data you manipulate in different parts of your app is valid.</p><h3>What&#8217;s next?</h3><p>Continue reading the next articles in the MVVM series:</p><ul><li><p><a href="https://techwood.substack.com/p/react-mvvm-view-layer">React MVVM Architecture with TanStack Router: View Layer</a></p></li><li><p><a href="https://techwood.substack.com/p/react-mvvm-application-layer">React MVVM Architecture with TanStack Router: Application Layer</a></p></li><li><p><a href="https://techwood.substack.com/p/react-mvvm-orchestrator">React MVVM Architecture with TanStack Router: Orchestrator</a></p></li></ul><p>Or go over the previous ones:</p><ul><li><p><a href="https://open.substack.com/pub/techwood/p/react-mvvm-with-tanstack-router?r=5uvuls&amp;utm_campaign=post&amp;utm_medium=web">React MVVM Architecture with TanStack Router: An Introduction</a></p></li><li><p><a href="https://open.substack.com/pub/techwood/p/react-mvvm-data-layer?r=5uvuls&amp;utm_campaign=post&amp;utm_medium=web">React MVVM Architecture with TanStack Router: Data Layer</a></p></li></ul><div><hr></div><p>Creating content like this is not an easy ride, so I hope you&#8217;ll hit that like button and drop a comment. As usual, constructive feedback is always welcome!</p><p>&#128279; <a href="https://www.linkedin.com/in/olexandr-pylypyshyn/">Follow me on LinkedIn</a></p><p>&#128240; Read me on the platform of your choice: <a href="https://techwood.substack.com/">Substack</a>, <a href="https://dev.to/techwood">DEV.to</a></p><p>Till next time &#128075;</p>]]></content:encoded></item><item><title><![CDATA[React MVVM Architecture with TanStack Router: Data Layer]]></title><description><![CDATA[Learn the React MVVM data layer: implement a testable repository pattern with TanStack Router/Query and Zustand in TypeScript, decouple APIs, and scale cleanly.]]></description><link>https://techwood.substack.com/p/react-mvvm-data-layer</link><guid isPermaLink="false">https://techwood.substack.com/p/react-mvvm-data-layer</guid><dc:creator><![CDATA[Olexandr]]></dc:creator><pubDate>Sun, 17 Aug 2025 20:20:16 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!fED-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc28bba4-383b-4831-b3e0-a8b18a2da47f_1920x1273.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!fED-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc28bba4-383b-4831-b3e0-a8b18a2da47f_1920x1273.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fED-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc28bba4-383b-4831-b3e0-a8b18a2da47f_1920x1273.jpeg 424w, https://substackcdn.com/image/fetch/$s_!fED-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc28bba4-383b-4831-b3e0-a8b18a2da47f_1920x1273.jpeg 848w, https://substackcdn.com/image/fetch/$s_!fED-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc28bba4-383b-4831-b3e0-a8b18a2da47f_1920x1273.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!fED-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc28bba4-383b-4831-b3e0-a8b18a2da47f_1920x1273.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!fED-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc28bba4-383b-4831-b3e0-a8b18a2da47f_1920x1273.jpeg" width="1456" height="965" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dc28bba4-383b-4831-b3e0-a8b18a2da47f_1920x1273.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:965,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:569311,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://techwood.substack.com/i/171214223?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc28bba4-383b-4831-b3e0-a8b18a2da47f_1920x1273.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!fED-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc28bba4-383b-4831-b3e0-a8b18a2da47f_1920x1273.jpeg 424w, https://substackcdn.com/image/fetch/$s_!fED-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc28bba4-383b-4831-b3e0-a8b18a2da47f_1920x1273.jpeg 848w, https://substackcdn.com/image/fetch/$s_!fED-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc28bba4-383b-4831-b3e0-a8b18a2da47f_1920x1273.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!fED-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc28bba4-383b-4831-b3e0-a8b18a2da47f_1920x1273.jpeg 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><figcaption class="image-caption">Repository. Photo by <a href="https://unsplash.com/@olenkasergienko?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Olena Bohovyk</a> on <a href="https://unsplash.com/photos/brown-wooden-door-with-silver-door-lever-iIX-jNXydeY?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></figcaption></figure></div><p>When developing your frontend application, most of the time you work with the data coming from or sending it somewhere. And as discussed in <a href="https://techwood.substack.com/p/react-mvvm-with-tanstack-router">this five-layer MVVM implementation</a>, you have to use the repository for these purposes. It sits just above the network or browser APIs, providing a clean, testable interface to the rest of your app.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!r4zk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d33038-997d-4791-ba73-c36da1bbed06_1341x813.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!r4zk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d33038-997d-4791-ba73-c36da1bbed06_1341x813.png 424w, https://substackcdn.com/image/fetch/$s_!r4zk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d33038-997d-4791-ba73-c36da1bbed06_1341x813.png 848w, https://substackcdn.com/image/fetch/$s_!r4zk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d33038-997d-4791-ba73-c36da1bbed06_1341x813.png 1272w, https://substackcdn.com/image/fetch/$s_!r4zk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d33038-997d-4791-ba73-c36da1bbed06_1341x813.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!r4zk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d33038-997d-4791-ba73-c36da1bbed06_1341x813.png" width="454" height="275.24384787472036" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b8d33038-997d-4791-ba73-c36da1bbed06_1341x813.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:813,&quot;width&quot;:1341,&quot;resizeWidth&quot;:454,&quot;bytes&quot;:90412,&quot;alt&quot;:&quot;Diagram showing the Data Layer in MVVM architecture. The Data Layer (Repositories) connects to three sources: API (cloud), Browser Storage (cylinder), and File (document icon).&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://techwood.substack.com/i/171214223?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d33038-997d-4791-ba73-c36da1bbed06_1341x813.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Diagram showing the Data Layer in MVVM architecture. The Data Layer (Repositories) connects to three sources: API (cloud), Browser Storage (cylinder), and File (document icon)." title="Diagram showing the Data Layer in MVVM architecture. The Data Layer (Repositories) connects to three sources: API (cloud), Browser Storage (cylinder), and File (document icon)." srcset="https://substackcdn.com/image/fetch/$s_!r4zk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d33038-997d-4791-ba73-c36da1bbed06_1341x813.png 424w, https://substackcdn.com/image/fetch/$s_!r4zk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d33038-997d-4791-ba73-c36da1bbed06_1341x813.png 848w, https://substackcdn.com/image/fetch/$s_!r4zk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d33038-997d-4791-ba73-c36da1bbed06_1341x813.png 1272w, https://substackcdn.com/image/fetch/$s_!r4zk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8d33038-997d-4791-ba73-c36da1bbed06_1341x813.png 1456w" sizes="100vw"></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><figcaption class="image-caption"><em>Data layer can talk to different sources from a single repository.</em></figcaption></figure></div><blockquote><p><strong>TIP:</strong> If you haven&#8217;t read <a href="https://open.substack.com/pub/techwood/p/react-mvvm-with-tanstack-router?r=5uvuls&amp;utm_campaign=post&amp;utm_medium=web">my MVVM Introduction</a> yet, I highly recommend doing that first. It&#8217;ll help you understand where data layer fits in the bigger picture.</p></blockquote><p>In this article, you&#8217;ll learn:</p><ul><li><p>What a repository is and when to use it.</p></li><li><p>Practical implementation with the source code.</p></li><li><p>How to write unit tests for the repository.</p></li></ul><p>By the end, you&#8217;ll know how to build a robust data layer that lets you scale your app without accumulating technical debt.</p><h2>What is a Repository?</h2><p>A repository is <a href="https://martinfowler.com/eaaCatalog/repository.html">a common design pattern</a> that allows you to encapsulate data-accessing logic. Some of the tasks for the repository may include:</p><ul><li><p>Communicate with the external storage.</p></li><li><p>Define a type for the raw response structure (e.g., JSON) returned from the external storage.</p></li><li><p>Handle storage-related errors, so the application layer can conveniently deal with them later.</p></li><li><p>Cache the data or delegate caching responsibility to third-party libraries.</p></li></ul><h2>When to use Repository?</h2><ul><li><p>When your app has to talk to two or more external endpoints.</p></li><li><p>When you want to isolate the data accessing logic from UI or business logic.</p></li><li><p>When there&#8217;s a breaking change in a third-party API, the repository is the only place where you&#8217;ll need to make changes.</p></li></ul><h2>Let&#8217;s roll up our sleeves &#128736;&#65039;</h2><p>To make this article and all the next ones in the series more engaging, we&#8217;ll develop a country flags quiz app. It will prompt the user with the flag, and they will have to guess the country. Then the system saves the answer to the localStorage and displays the answer history in the left sidebar. You can find <a href="https://github.com/ole-techwood/articles/tree/mvvm">the source code in this GitHub repository</a> and a <a href="https://codesandbox.io/p/github/ole-techwood/articles/mvvm">Demo on CodeSandbox</a>.</p><p><a href="https://tanstack.com/router/latest/docs/framework/react/quick-start">Bootstrap a new TanStack Router app</a> using the package manager of your choice. Next, remove all the redundant files (in our case: <code>reportWebVitals.ts</code>, <code>logo.svg</code>, and so on) and you&#8217;re good to go.</p><h2>Your first repository</h2><p>Okay, I&#8217;m not sure it&#8217;s your first-ever repository, but at least it is in this architecture series:</p><pre><code>import { apiServiceFactory, type APIService } from "@/shared/api";

/**
 * The repository for the Home module of the app.
 */
export class HomeRepository {
  /**
   * Classes that we inject into the repository through the constructor.
   */
  private readonly apiService: APIService;

  constructor(apiService: APIService) {
    this.apiService = apiService;
  }

  /**
   * Repository's method to find all avaliable countries.
   *
   * Notice that we didn't specify a proper type for the result 
   * of `get` method (used generic `any` instead).
   * We'll do it later in the model layer article.
   */
  findCountries() {
    return this.apiService.get&lt;any[]&gt;("/all?fields=name,flags");
  }
}

/**
 * Factory method to spawn a new instance of HomeRepository and avoid singletone
 *
 * It also injects `APIService` into the `HomeRepository`.
 * Keep reading to get what `APIService` is.
 */
export const homeRepositoryFactory = () =&gt; {
  const apiService = apiServiceFactory(["Home"]);

  return new HomeRepository(apiService);
};</code></pre><p>I know, a lot of code right off the bat, but in a nutshell, the repository is usually a class with a couple of private fields, a constructor, and methods. This repository is called <code>HomeRepository</code> because it&#8217;s a part of the <code>Home</code> module where the quiz happens.</p><blockquote><p><strong>NOTE:</strong> There&#8217;s no strict rule saying a repository has to be a class. I&#8217;ll explain that in the article down below.</p></blockquote><p>At the moment, your folder structure within the src directory should look like this:</p><pre><code>src/
&#9500;&#9472;&#9472; routes/
&#9474;   &#9500;&#9472;&#9472; __root.tsx
&#9474;   &#9492;&#9472;&#9472; index.tsx
&#9492;&#9472;&#9472; modules/
    &#9492;&#9472;&#9472; Home/
        &#9492;&#9472;&#9472; data/
            &#9492;&#9472;&#9472; home.repository.ts</code></pre><p>Now let&#8217;s focus on interesting tidbits &#128071;</p><h3>Make all repositories dumb</h3><p>Wait what? Don&#8217;t we, as smart developers, write smart programs for smart people? Yes, we do. But here&#8217;s the paradox: we need to make the individual parts of our programs dumb and abstract, so the whole system is intelligent. Our repository is not an exception!</p><p>A repository should never know which library you&#8217;re using: axios, fetch, or another-shiny-library. Right now, we have one repository, and things are simple. But imagine our country quiz app blew up - we raised billions, devs are no longer hungry, and they added 100+ repositories. Each one uses axios directly. One day, the CTO comes and says: &#8220;NewHypingHttpLibrary just came up, it&#8217;s 100x faster and more secure, we need to migrate ASAP&#8221;. Damn, now you are in &#8220;you vs 100-repositories-to-be-refactored-tomorrow&#8221; position.</p><p>To avoid such situations, we introduce a special class called <code>APIService</code>:</p><pre><code>import { queryOptions } from "@tanstack/react-query";
import axios, { type AxiosInstance } from "axios";

/**
 * A facade over HTTP library to perform API requests.
 */
export class APIService {
  /**
   * The Axios client instance used to perform HTTP requests.
   */
  private readonly apiClient: AxiosInstance;

  /**
   * Query key provided by the consumer of the `APIService` class.
   */
  private readonly queryKey: string[];

  constructor(apiClient: AxiosInstance, queryKey: string[]) {
    this.apiClient = apiClient;
    this.queryKey = queryKey;
  }

  /**
   * Executes a GET request to the provided API endpoint
   *
   * @template D - The expected response data type.
   *
   * @param endpoint - The path to the API endpoint.
   *
   * @returns Query options object for the use with TanStack Query.
   */
  get&lt;D&gt;(endpoint: string) {
    return queryOptions({
      queryKey: this.queryKey,
      queryFn: () =&gt; this.apiClient.get&lt;D&gt;(endpoint),
    });
  }
}

/**
 * Factory function to create a configured instance of the `APIService` class.
 *
 * @param queryKey - The query key array used for caching and identifying queries.
 *
 * @returns A configured `APIService` instance ready for making API requests.
 */
export const apiServiceFactory = (queryKey: string[]) =&gt; {
  const apiClient = axios.create({
    baseURL: "https://restcountries.com/v3.1",
    adapter: "fetch",
  });

  return new APIService(apiClient, queryKey);
};</code></pre><p>You can put <code>api.service.ts</code> wherever you want, but I&#8217;ll put in <code>src/shared/api</code> to indicate that this is an API-related piece of code shared across the modules:</p><pre><code>src/
&#9500;&#9472;&#9472; modules/
&#9474;   &#9492;&#9472;&#9472; Home/
&#9474;       &#9492;&#9472;&#9472; data/
&#9474;           &#9492;&#9472;&#9472; home.repository.ts
&#9500;&#9472;&#9472; routes/
&#9474;   &#9500;&#9472;&#9472; __root.tsx
&#9474;   &#9492;&#9472;&#9472; index.tsx
&#9500;&#9472;&#9472; shared/
&#9474;   &#9492;&#9472;&#9472; api/
&#9474;       &#9500;&#9472;&#9472; api.service.ts
&#9474;       &#9492;&#9472;&#9472; index.ts <em>- index file for re-exporting</em></code></pre><p>Now <code>HomeRepository</code> and other repositories depend on the abstract <code>APIService</code> interface, not a concrete implementation. That way, you can easily replace the underlying HTTP library (like axios or fetch) or make other internal 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_!d-fY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F692a5cfb-4ccf-49a8-a809-c696e51a6949_1461x1113.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!d-fY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F692a5cfb-4ccf-49a8-a809-c696e51a6949_1461x1113.png 424w, https://substackcdn.com/image/fetch/$s_!d-fY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F692a5cfb-4ccf-49a8-a809-c696e51a6949_1461x1113.png 848w, https://substackcdn.com/image/fetch/$s_!d-fY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F692a5cfb-4ccf-49a8-a809-c696e51a6949_1461x1113.png 1272w, https://substackcdn.com/image/fetch/$s_!d-fY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F692a5cfb-4ccf-49a8-a809-c696e51a6949_1461x1113.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!d-fY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F692a5cfb-4ccf-49a8-a809-c696e51a6949_1461x1113.png" width="506" height="385.407967032967" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/692a5cfb-4ccf-49a8-a809-c696e51a6949_1461x1113.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1109,&quot;width&quot;:1456,&quot;resizeWidth&quot;:506,&quot;bytes&quot;:145615,&quot;alt&quot;:&quot;Diagram of three consumers connecting to repositories, which call a shared APIService using Axios for requests.&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;:&quot;https://techwood.substack.com/i/171214223?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F692a5cfb-4ccf-49a8-a809-c696e51a6949_1461x1113.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Diagram of three consumers connecting to repositories, which call a shared APIService using Axios for requests." title="Diagram of three consumers connecting to repositories, which call a shared APIService using Axios for requests." srcset="https://substackcdn.com/image/fetch/$s_!d-fY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F692a5cfb-4ccf-49a8-a809-c696e51a6949_1461x1113.png 424w, https://substackcdn.com/image/fetch/$s_!d-fY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F692a5cfb-4ccf-49a8-a809-c696e51a6949_1461x1113.png 848w, https://substackcdn.com/image/fetch/$s_!d-fY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F692a5cfb-4ccf-49a8-a809-c696e51a6949_1461x1113.png 1272w, https://substackcdn.com/image/fetch/$s_!d-fY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F692a5cfb-4ccf-49a8-a809-c696e51a6949_1461x1113.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><figcaption class="image-caption"><em>If we swap axios, it won&#8217;t break repositories or consumers. Because they communicate with the APIService, not with the library directly.</em></figcaption></figure></div><blockquote><p><strong>NOTE:</strong> This approach satisfies Dependency Inversion principle of SOLID. <a href="https://khalilstemmler.com/articles/solid-principles/solid-typescript">Find out more about SOLID in TypeScript</a>.</p></blockquote><h3>Querying on steroids</h3><p>In the <code>get</code> method of <code>APIService</code>, you may notice that we return the result of the <a href="https://tanstack.com/query/latest/docs/framework/react/reference/queryOptions">queryOptions</a> function:</p><pre><code>get&lt;D&gt;(endpoint: string) {
  return queryOptions({
    queryKey: this.queryKey,
    queryFn: () =&gt; this.apiClient.get&lt;D&gt;(endpoint),
  });
}</code></pre><p>That&#8217;s because I propose using <a href="https://tanstack.com/query/latest/docs/framework/react/overview">TanStack Query</a> for making HTTP requests in our app. It will handle all the errors and cache the data for us, so we don&#8217;t have to write and test this logic ourselves. You can find all the benefits the library provides in the link I&#8217;ve put in the previous sentence.</p><blockquote><p><strong>NOTE:</strong> I&#8217;ll rely on TanStack Query in this article and all the next ones, though you&#8217;re free to use another data-fetching library, or not to use any.</p></blockquote><h3>A repository <strong>doesn&#8217;t always have to be</strong> a class</h3><p>As mentioned before, a repository is an interface to communicate with the external data source. And there&#8217;s no strict rule that it has to be a class. Basically, it can be anything: a function, a class, or even a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules">JavaScript module</a> exposing pure methods. As long as it encapsulates access to the external data, it&#8217;s fine.</p><p>For example, we agreed that we&#8217;ll save the history in localStorage. To make it easy to work with this browser API, we can use <a href="https://zustand.docs.pmnd.rs/getting-started/introduction">Zustand</a> and its <a href="https://zustand.docs.pmnd.rs/middlewares/persist">persist</a> and <a href="https://zustand.docs.pmnd.rs/integrations/persisting-store-data#createjsonstorage">createJSONStorage</a> functions.</p><blockquote><p><strong>NOTE:</strong> In this article, I won&#8217;t explain how to set up Zustand. You can read the official documentation or watch this <a href="https://www.youtube.com/watch?v=_ngCLZ5Iz-0">Zustand video tutorial</a> by Cosden Solutions. You can also find <a href="https://github.com/ole-techwood/articles/blob/mvvm/src/shared/store/store.ts">the configured store on GitHub</a>. Pay attention to where I put it!</p></blockquote><p>Let&#8217;s create a <code>historySlice</code> that will be a part of the <code>History</code> module:</p><pre><code>import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";

export type HistorySlice = {
  history: any[];
  saveAnswer: (result: any) =&gt; void;
  clearHistory: () =&gt; void;
};

export const historySlice = create&lt;HistorySlice&gt;()(
  persist(
    (set) =&gt; ({
      history: [],

      // Saves the user's answer to the localStorage
      saveAnswer: (answer) =&gt;
        set((state) =&gt; ({ history: [answer, ...state.history] })),

      // Clears the history from the localStorage
      clearHistory: () =&gt; set({ history: [] }),
    }),
    {
      // localStorage key
      name: "history",

      // Tells Zustand to use localStorage as a persistent storage for our data
      storage: createJSONStorage(() =&gt; localStorage),
    }
  )
);</code></pre><p>This is another example of an elegant repository:</p><ul><li><p>We avoid calling localStorage directly, utilizing the API provided by the repository.</p></li><li><p>Each slice is a different function that we can independently develop and test.</p></li><li><p>Zustand enables reactive updates, which reduces the need for manual data synchronization between the localStorage consumers.</p></li></ul><p>Now you can either <a href="https://zustand.docs.pmnd.rs/guides/updating-state">use historySlice directly in your code</a> or wrap it in <code>HistoryRepository</code>, which I recommend:</p><p>Keep in mind that it&#8217;s an optional step, and you can skip creating an additional class if you feel like it&#8217;s overkill for you.</p><pre><code>import { historySlice } from "./history.slice";

/**
 * Repository for managing history in browser storage.
 */
export class HistoryRepository {
  /**
   * Slice used to access the data.
   */
  private readonly slice: typeof historySlice;

  constructor(slice: typeof historySlice) {
    this.slice = slice;
  }

  /**
   * Saves a new result to the beginning of the history array.
   * 
   * Append `createdAt` timestamp to indicate when the record was created
   *
   * @param createHistoryRecordDto - The new result to save
   */
  saveAnswer(createHistoryRecordDto: any) {
    return this.slice
      .getState()
      .saveAnswer({ ...createHistoryRecordDto, createdAt: Date.now() });
  }

  /**
   * Returns an array of all saved history records.
   * If no history exists, returns an empty array.
   *
   * @returns Array of history records.
   */
  getHistory() {
    return this.slice((state) =&gt; state.history);
  }
}

/**
 * Factory function to create a HistoryRepository instance.
 *
 * @returns A new HistoryRepository instance
 */
export const historyRepositoryFactory = () =&gt;
  new HistoryRepository(historySlice);
</code></pre><p>Still two main reasons to do it:</p><ol><li><p>Any consumer of the data layer will always know - the repository <em>class</em> is always the main entry point into the layer. They won&#8217;t need to care whether it&#8217;s Zustand, localStorage, or anything else under the hood.</p></li><li><p>Even better DI - you can easily replace Zustand with another state management library if you ever need it.</p></li></ol><p>At this point, your modules folder structure should look like this:</p><pre><code>src/
&#9492;&#9472;&#9472; modules/
    &#9500;&#9472;&#9472; Home/
    &#9474;   &#9492;&#9472;&#9472; data/
    &#9474;       &#9492;&#9472;&#9472; home.repository.ts
    &#9492;&#9472;&#9472; History/
        &#9492;&#9472;&#9472; data/
            &#9500;&#9472;&#9472; history.repository.ts
            &#9500;&#9472;&#9472; history.slice.ts
            &#9492;&#9472;&#9472; index.ts - index file for re-exporting</code></pre><p>And that wraps up the repository implementation. Now let&#8217;s make sure everything behaves correctly by writing some tests &#129514;.</p><h2>Testing</h2><blockquote><p><strong>NOTE:</strong> I&#8217;ll use <a href="https://vitest.dev/guide/">Vitest</a> as a testing framework.</p></blockquote><p>First, let&#8217;s write unit tests for <code>HomeRepository</code>:</p><pre><code>import type { APIService } from "@/shared/api/api.service";
import { describe, expect, it, vi } from "vitest";
import { HomeRepository, homeRepositoryFactory } from "../home.repository";

// Mock APIService
function createMockApiService(getImpl?: any): APIService {
  return {
    get: getImpl || vi.fn(),
  } as unknown as APIService;
}

describe("HomeRepository", () =&gt; {
  let apiService: APIService;

  describe("findCountries", () =&gt; {
    it("should return countries", async () =&gt; {
      const mockCountries = [
        {
          name: {
            common: "Ukraine",
            official: "Ukraine",
            nativeName: {
              ukr: { official: "&#1059;&#1082;&#1088;&#1072;&#1111;&#1085;&#1072;", common: "&#1059;&#1082;&#1088;&#1072;&#1111;&#1085;&#1072;" },
            },
          },
          flags: {
            png: "ua.png",
            svg: "ua.svg",
            alt: "Flag of Ukraine",
          },
        },
        {
          name: {
            common: "Poland",
            official: "Republic of Poland",
            nativeName: {
              pol: { official: "Rzeczpospolita Polska", common: "Polska" },
            },
          },
          flags: {
            png: "pl.png",
            svg: "pl.svg",
            alt: "Flag of Poland",
          },
        },
      ];

      apiService = createMockApiService(
        vi.fn().mockResolvedValue(mockCountries)
      );

      // Easily inject any APIService into HomeRepository
      const repo = new HomeRepository(apiService);
      const result = await repo.findCountries();

      expect(result).toEqual(mockCountries);
    });

    it("should return empty array", async () =&gt; {
      apiService = createMockApiService(vi.fn().mockResolvedValue([]));

      const repo = new HomeRepository(apiService);
      const result = await repo.findCountries();

      expect(result).toEqual([]);
    });

    it("should fail when API throws", async () =&gt; {
      apiService = createMockApiService(
        vi.fn().mockRejectedValue(new Error("API fail"))
      );
      const repo = new HomeRepository(apiService);
      await expect(repo.findCountries()).rejects.toThrow("API fail");
    });
  });
});

describe("homeRepositoryFactory", () =&gt; {
  it("should return HomeRepository instance with working apiService", () =&gt; {
    const repo = homeRepositoryFactory();

    expect(repo).toBeDefined();
    expect(typeof repo.findCountries).toBe("function");
  });
});</code></pre><p>Notice how straightforward it is to test the repository since we made it &#8220;dumb&#8221; earlier:</p><pre><code>it("should return empty array", async () =&gt; {
  apiService = createMockApiService(vi.fn().mockResolvedValue([]));

  const repo = new HomeRepository(apiService);
  const result = await repo.findCountries();

  expect(result).toEqual([]);
});</code></pre><p>We can inject any specimen of the <code>APIService</code> into the <code>HomeRepository</code>. This shows Dependency Inversion in action. The repository just needs an object that fits the <code>APIService</code> interface, which makes mocking and testing a piece of cake.</p><p>Here&#8217;s how we can test <code>historySlice</code>:</p><pre><code>import { beforeEach, describe, expect, it } from "vitest";
import { historySlice } from "../history.slice";

// Mock localStorage for Zustand persist
const localStorageMock = (() =&gt; {
  let store: Record&lt;string, string&gt; = {};

  return {
    getItem: (key: string) =&gt; store[key] || null,
    setItem: (key: string, value: string) =&gt; {
      store[key] = value;
    },
    removeItem: (key: string) =&gt; {
      delete store[key];
    },
    clear: () =&gt; {
      store = {};
    },
  };
})();

Object.defineProperty(window, "localStorage", { value: localStorageMock });

describe("historySlice", () =&gt; {
  let slice: typeof historySlice;

  const sample = {
    flagImage: "flag.png",
    countryName: "Ukraine",
    userAnswer: "Ukraine",
  };

  // Start from scratch each test case - clear the history
  beforeEach(() =&gt; {
    window.localStorage.clear();

    slice = historySlice;
    slice.getState().clearHistory();
  });

  it("should initialize with empty history", () =&gt; {
    expect(slice.getState().history).toEqual([]);
  });

  it("should save an answer to history", () =&gt; {
    slice.getState().saveAnswer(sample);

    expect(slice.getState().history[0]).toEqual(sample);
  });

  it("should save multiple answers in correct order", () =&gt; {
    const answer = {
      flagImage: "b.png",
      countryName: "Poland",
      userAnswer: "Poland",
    };

    // sample is inserted first, but should appear last in the final array
    slice.getState().saveAnswer(sample);

    // answer is inserted last, but should appear first in the final array
    slice.getState().saveAnswer(answer);

    expect(slice.getState().history[0]).toEqual(answer);

    // sample is second, because Zustand append new value at the start of the array
    expect(slice.getState().history[1]).toEqual(sample);
  });

  it("should clear history", () =&gt; {
    slice.getState().saveAnswer(sample);
    slice.getState().clearHistory();

    expect(slice.getState().history).toEqual([]);
  });
});</code></pre><p>Because Zustand persists in the localStorage, we need to mock it in test environments that lack browsers&#8217; API:</p><pre><code>const localStorageMock = (() =&gt; {
  let store: Record&lt;string, string&gt; = {};

  return {
    getItem: (key: string) =&gt; store[key] || null,
    setItem: (key: string, value: string) =&gt; {
      store[key] = value;
    },
    removeItem: (key: string) =&gt; {
      delete store[key];
    },
    clear: () =&gt; {
      store = {};
    },
  };
})();

Object.defineProperty(window, "localStorage", { value: localStorageMock });</code></pre><p>As you can see, test cases use regular Zustand syntax to assert the expected results:</p><p>As a little practice task, try to write unit tests for <code>HistoryRepository</code> on your own. Find <a href="https://github.com/ole-techwood/articles/blob/mvvm/src/modules/History/data/tests/history.repository.test.ts">my implementation of tests for the history repository on GitHub</a>.</p><h2>Summary</h2><p>You made it! &#128293;</p><p>Now you know:</p><ul><li><p>What a repository is and how to use it.</p></li><li><p>Why the dependency inversion principle is important and how the repository allows you to fulfill it.</p></li></ul><p>Additionally, you&#8217;ve learned how to utilize modern libraries like TanStack Query and Zustand to make your repositories more powerful than ever.</p><p>As icing on the cake, we discussed how to test different kinds of repositories. You saw how effortless it is to write unit tests when the repo follows clean architecture.</p><h3>What&#8217;s next?</h3><p>Deep dive into the series to grasp the whole picture about <a href="https://open.substack.com/pub/techwood/p/react-mvvm-with-tanstack-router?r=5uvuls&amp;utm_campaign=post&amp;utm_medium=web">modern MVVM architecture in React</a>:</p><ul><li><p><a href="https://techwood.substack.com/p/react-mvvm-model-layer">React MVVM Architecture with TanStack Router: Model Layer</a></p></li><li><p><a href="https://techwood.substack.com/p/react-mvvm-view-layer">React MVVM Architecture with TanStack Router: View Layer</a></p></li><li><p><a href="https://techwood.substack.com/p/react-mvvm-application-layer">React MVVM Architecture with TanStack Router: Application Layer</a></p></li><li><p><a href="https://techwood.substack.com/p/react-mvvm-orchestrator">React MVVM Architecture with TanStack Router: Orchestrator</a></p></li></ul><div><hr></div><p>Creating content like this is not an easy ride, so I hope you&#8217;ll hit that like button and drop a comment. As usual, constructive feedback is always welcome!</p><p>&#128279; <a href="https://www.linkedin.com/in/olexandr-pylypyshyn/">Follow me on LinkedIn</a></p><p>&#128240; Read me on the platform of your choice: <a href="https://techwood.substack.com/">Substack</a>, <a href="https://dev.to/techwood">DEV.to</a></p><p>Till next time &#128075;</p>]]></content:encoded></item><item><title><![CDATA[React MVVM Architecture with TanStack Router: An Introduction]]></title><description><![CDATA[Learn how to implement clean MVVM architecture in React with TanStack Router. A practical guide to layering data, logic, and UI for scalable apps.]]></description><link>https://techwood.substack.com/p/react-mvvm-with-tanstack-router</link><guid isPermaLink="false">https://techwood.substack.com/p/react-mvvm-with-tanstack-router</guid><dc:creator><![CDATA[Olexandr]]></dc:creator><pubDate>Sun, 17 Aug 2025 19:20:20 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!4hXY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b71f18-6117-48eb-9c1c-7afd434ce21c_1920x1280.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4hXY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b71f18-6117-48eb-9c1c-7afd434ce21c_1920x1280.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4hXY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b71f18-6117-48eb-9c1c-7afd434ce21c_1920x1280.jpeg 424w, https://substackcdn.com/image/fetch/$s_!4hXY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b71f18-6117-48eb-9c1c-7afd434ce21c_1920x1280.jpeg 848w, https://substackcdn.com/image/fetch/$s_!4hXY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b71f18-6117-48eb-9c1c-7afd434ce21c_1920x1280.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!4hXY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b71f18-6117-48eb-9c1c-7afd434ce21c_1920x1280.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4hXY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b71f18-6117-48eb-9c1c-7afd434ce21c_1920x1280.jpeg" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d8b71f18-6117-48eb-9c1c-7afd434ce21c_1920x1280.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:260931,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://techwood.substack.com/i/171201660?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b71f18-6117-48eb-9c1c-7afd434ce21c_1920x1280.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4hXY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b71f18-6117-48eb-9c1c-7afd434ce21c_1920x1280.jpeg 424w, https://substackcdn.com/image/fetch/$s_!4hXY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b71f18-6117-48eb-9c1c-7afd434ce21c_1920x1280.jpeg 848w, https://substackcdn.com/image/fetch/$s_!4hXY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b71f18-6117-48eb-9c1c-7afd434ce21c_1920x1280.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!4hXY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8b71f18-6117-48eb-9c1c-7afd434ce21c_1920x1280.jpeg 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><figcaption class="image-caption">Clean architecture. Photo by <a href="https://unsplash.com/@pawel_czerwinski?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Pawel Czerwinski</a> on <a href="https://unsplash.com/photos/white-building-NVJaDezs4FU?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></figcaption></figure></div><p>If you&#8217;re reading this article, you&#8217;ve probably already understood the importance of having clean architecture within your app. And if you&#8217;ve tried applying those clean architecture principles to your React front-end, you&#8217;ve likely hit a wall (I know, I&#8217;ve been there too).</p><p>In this blog post, I don&#8217;t want in a million time explain <em>why</em> clean architecture matters, like so many great authors did before me. I want to focus on rather <em>how</em> to implement it, specifically on the front-end using React and <a href="https://tanstack.com/router/latest/docs/framework/react/overview">TanStack Router</a>.</p><p>I&#8217;ve been carefully crafting and experimenting with the approach I&#8217;m about to show you for years on dozens of production and side projects. My goal now is to help you implement the same approach in your own projects.</p><blockquote><p><strong>TIP:</strong> If you want to understand fundamentals of software architecture, I recommend choosing the book called <a href="https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164">&#8220;Clean Architecture: A Craftsman's Guide to Software Structure and Design&#8221; by R. Martin</a> as a starting point.</p></blockquote><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://techwood.substack.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 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><h2>First things first: Why TanStack Router?</h2><p>TanStack Router is not architecturally biased.</p><p>While it comes with some great features like a file-based routing system and router-related code generation, regarding the architectural aspects, the official documentation doesn&#8217;t provide you with any information. But that&#8217;s exactly what I like about TanStack Router: it&#8217;s a robust yet highly adaptable client-side framework. You can build any architecture with it, including the hero of today&#8217;s article.</p><h2>Ladies and gents, meet MVVM</h2><p>MVVM is a common design pattern that you can use to:</p><ul><li><p><strong>Have a clear separation of concerns</strong> between UI, business logic, data accessing, and interfaces or types.</p></li><li><p><strong>Integrate tools like <a href="https://plopjs.com/">plop.js</a></strong> to automate the creation of the new <em>modules</em> within your app with all the required concerns.</p></li><li><p><strong>Simplify unit tests writing</strong> by focusing on one piece of functionality at a time. It simply means that with MVVM, you can test business logic without worrying about UI and vice versa.</p></li></ul><blockquote><p><strong>NOTE:</strong> When I say <em>&#8220;modules within your app&#8221;</em>, I mean Authentication, Shopping Cart, Home, About and other <em>pages</em> in your app. You might also know them as <em>features.</em></p></blockquote><h3>How can MVVM gift me all these sweets?</h3><p>By breaking all the <strong>modules</strong> in your app into:</p><ul><li><p><strong>Models -</strong> a place where data access and business logic handling are happening.</p></li><li><p><strong>Views -</strong> UI (widgets or components).</p></li><li><p><strong>ViewModels -</strong> serve as an orchestrator between models and views.</p></li></ul><h3>More practical approach</h3><p>Above, I&#8217;ve described the theory of MVVM with <em>three</em> core parts. In practice, I break MVVM into <em>five</em> more granular layers:</p><ul><li><p><strong>Data</strong>, <strong>Application,</strong> and <strong>Model</strong> correspond to the Model layer in the original MVVM.</p></li><li><p><strong>View</strong> stays the same in both the original and the implementation I&#8217;m about to show you.</p></li><li><p><strong>Orchestrating component</strong> used as a ViewModel.</p></li></ul><p>This way, each concern stays isolated, making things easier to test and maintain. Each layer handles its specific task, and there's a clear way in which they interact with one another.</p><blockquote><p><strong>NOTE:</strong> Naming of the layers is totally up to you. I just recommend keeping it consistent to ensure clarity and speed up onboarding.</p></blockquote><p>Here&#8217;s the preview:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TKZD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5fcdcca-2f9e-4d6c-b8ea-7f25c5d3d61e_723x2343.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TKZD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5fcdcca-2f9e-4d6c-b8ea-7f25c5d3d61e_723x2343.png 424w, https://substackcdn.com/image/fetch/$s_!TKZD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5fcdcca-2f9e-4d6c-b8ea-7f25c5d3d61e_723x2343.png 848w, https://substackcdn.com/image/fetch/$s_!TKZD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5fcdcca-2f9e-4d6c-b8ea-7f25c5d3d61e_723x2343.png 1272w, https://substackcdn.com/image/fetch/$s_!TKZD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5fcdcca-2f9e-4d6c-b8ea-7f25c5d3d61e_723x2343.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TKZD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5fcdcca-2f9e-4d6c-b8ea-7f25c5d3d61e_723x2343.png" width="241" height="781" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b5fcdcca-2f9e-4d6c-b8ea-7f25c5d3d61e_723x2343.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:2343,&quot;width&quot;:723,&quot;resizeWidth&quot;:241,&quot;bytes&quot;:217373,&quot;alt&quot;:&quot;Diagram illustrating React MVVM architecture with five layers. A cloud &#8220;Data Source&#8221; connects to the Data Layer (Repositories). Below, the Model Layer shows DTO &amp; Payload, Module Type, and Prop types. The Application Layer (Hooks) passes data to the View Layer (Widgets). An Orchestrator wraps all layers, showing the flow from data source through repositories, models, hooks, and finally to UI widgets.&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;:&quot;https://techwood.substack.com/i/171201660?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5fcdcca-2f9e-4d6c-b8ea-7f25c5d3d61e_723x2343.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-normal" alt="Diagram illustrating React MVVM architecture with five layers. A cloud &#8220;Data Source&#8221; connects to the Data Layer (Repositories). Below, the Model Layer shows DTO &amp; Payload, Module Type, and Prop types. The Application Layer (Hooks) passes data to the View Layer (Widgets). An Orchestrator wraps all layers, showing the flow from data source through repositories, models, hooks, and finally to UI widgets." title="Diagram illustrating React MVVM architecture with five layers. A cloud &#8220;Data Source&#8221; connects to the Data Layer (Repositories). Below, the Model Layer shows DTO &amp; Payload, Module Type, and Prop types. The Application Layer (Hooks) passes data to the View Layer (Widgets). An Orchestrator wraps all layers, showing the flow from data source through repositories, models, hooks, and finally to UI widgets." srcset="https://substackcdn.com/image/fetch/$s_!TKZD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5fcdcca-2f9e-4d6c-b8ea-7f25c5d3d61e_723x2343.png 424w, https://substackcdn.com/image/fetch/$s_!TKZD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5fcdcca-2f9e-4d6c-b8ea-7f25c5d3d61e_723x2343.png 848w, https://substackcdn.com/image/fetch/$s_!TKZD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5fcdcca-2f9e-4d6c-b8ea-7f25c5d3d61e_723x2343.png 1272w, https://substackcdn.com/image/fetch/$s_!TKZD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5fcdcca-2f9e-4d6c-b8ea-7f25c5d3d61e_723x2343.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><figcaption class="image-caption"><em>Arrows show how layers depend on each other.</em></figcaption></figure></div><p>Now let&#8217;s take a closer look at each layer.&#128071;</p><blockquote><p><strong>NOTE:</strong> Because there's a lot to explain, this article focuses on a broad, high-level look at the layers. You can find links below to individual articles that dive deeper into each layer with code examples. I recommend to follow the order of layers beneath, when exploring each one individually. Thus you don&#8217;t miss anything and understand all the details.</p></blockquote><h2>Data Layer</h2><p>This layer communicates with the external source(-s). For example, it can perform API calls, make requests to localStorage, and read or write data into a file. Think of the Data Layer as &#8220;raw&#8221; access: it only knows <em>how</em> to fetch.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!N19m!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79c296b4-443a-455c-807e-d4e4f82cb3c1_1341x813.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!N19m!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79c296b4-443a-455c-807e-d4e4f82cb3c1_1341x813.png 424w, https://substackcdn.com/image/fetch/$s_!N19m!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79c296b4-443a-455c-807e-d4e4f82cb3c1_1341x813.png 848w, https://substackcdn.com/image/fetch/$s_!N19m!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79c296b4-443a-455c-807e-d4e4f82cb3c1_1341x813.png 1272w, https://substackcdn.com/image/fetch/$s_!N19m!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79c296b4-443a-455c-807e-d4e4f82cb3c1_1341x813.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!N19m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79c296b4-443a-455c-807e-d4e4f82cb3c1_1341x813.png" width="448" height="271.6062639821029" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/79c296b4-443a-455c-807e-d4e4f82cb3c1_1341x813.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:813,&quot;width&quot;:1341,&quot;resizeWidth&quot;:448,&quot;bytes&quot;:90412,&quot;alt&quot;:&quot;Diagram showing the Data Layer in MVVM architecture. The Data Layer (Repositories) connects to three sources: API (cloud), Browser Storage (cylinder), and File (document icon).&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;:&quot;https://techwood.substack.com/i/171201660?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79c296b4-443a-455c-807e-d4e4f82cb3c1_1341x813.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Diagram showing the Data Layer in MVVM architecture. The Data Layer (Repositories) connects to three sources: API (cloud), Browser Storage (cylinder), and File (document icon)." title="Diagram showing the Data Layer in MVVM architecture. The Data Layer (Repositories) connects to three sources: API (cloud), Browser Storage (cylinder), and File (document icon)." srcset="https://substackcdn.com/image/fetch/$s_!N19m!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79c296b4-443a-455c-807e-d4e4f82cb3c1_1341x813.png 424w, https://substackcdn.com/image/fetch/$s_!N19m!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79c296b4-443a-455c-807e-d4e4f82cb3c1_1341x813.png 848w, https://substackcdn.com/image/fetch/$s_!N19m!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79c296b4-443a-455c-807e-d4e4f82cb3c1_1341x813.png 1272w, https://substackcdn.com/image/fetch/$s_!N19m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79c296b4-443a-455c-807e-d4e4f82cb3c1_1341x813.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><figcaption class="image-caption"><em>Data layer can talk to different sources from a single repository.</em></figcaption></figure></div><p>The layer contains a well-known class(-es) called the <strong>repository pattern,</strong> which is used to access the external data. The repository has methods, each representing one operation.</p><p>Suppose we need to fetch a list of all products. We&#8217;d create a method inside the repository called <code>findAllProducts</code> which:</p><ol><li><p>Makes a GET request to an external source.</p></li><li><p>Defines a type for the raw JSON response.</p></li><li><p>Returns type-safe data to the consumer who called the method.</p></li></ol><p><a href="https://techwood.substack.com/p/react-mvvm-data-layer">Learn more about the Data layer and repositories.</a></p><h2>Model Layer</h2><p>Now that we&#8217;ve fetched the data, let&#8217;s add some types for it.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Pz50!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cf12c5d-4bca-45e8-aff8-189bc619e3ab_603x618.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Pz50!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cf12c5d-4bca-45e8-aff8-189bc619e3ab_603x618.png 424w, https://substackcdn.com/image/fetch/$s_!Pz50!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cf12c5d-4bca-45e8-aff8-189bc619e3ab_603x618.png 848w, https://substackcdn.com/image/fetch/$s_!Pz50!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cf12c5d-4bca-45e8-aff8-189bc619e3ab_603x618.png 1272w, https://substackcdn.com/image/fetch/$s_!Pz50!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cf12c5d-4bca-45e8-aff8-189bc619e3ab_603x618.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Pz50!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cf12c5d-4bca-45e8-aff8-189bc619e3ab_603x618.png" width="203" height="208.0497512437811" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6cf12c5d-4bca-45e8-aff8-189bc619e3ab_603x618.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:618,&quot;width&quot;:603,&quot;resizeWidth&quot;:203,&quot;bytes&quot;:60508,&quot;alt&quot;:&quot;Illustration of the Model layer in React MVVM architecture. It contains three blue blocks labeled &#8220;DTO &amp; Payload,&#8221; &#8220;Module Type,&#8221; and &#8220;Prop types.&#8221;&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;:&quot;https://techwood.substack.com/i/171201660?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cf12c5d-4bca-45e8-aff8-189bc619e3ab_603x618.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Illustration of the Model layer in React MVVM architecture. It contains three blue blocks labeled &#8220;DTO &amp; Payload,&#8221; &#8220;Module Type,&#8221; and &#8220;Prop types.&#8221;" title="Illustration of the Model layer in React MVVM architecture. It contains three blue blocks labeled &#8220;DTO &amp; Payload,&#8221; &#8220;Module Type,&#8221; and &#8220;Prop types.&#8221;" srcset="https://substackcdn.com/image/fetch/$s_!Pz50!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cf12c5d-4bca-45e8-aff8-189bc619e3ab_603x618.png 424w, https://substackcdn.com/image/fetch/$s_!Pz50!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cf12c5d-4bca-45e8-aff8-189bc619e3ab_603x618.png 848w, https://substackcdn.com/image/fetch/$s_!Pz50!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cf12c5d-4bca-45e8-aff8-189bc619e3ab_603x618.png 1272w, https://substackcdn.com/image/fetch/$s_!Pz50!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cf12c5d-4bca-45e8-aff8-189bc619e3ab_603x618.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption"><em>Model layer includes Payloads &amp; DTOs, Module Type, and prop types.</em></figcaption></figure></div><p>The Model layer defines types, interfaces, and classes, including:</p><h3>Payloads</h3><p>Interfaces or types that define a structure for the untyped data (like JSON) we receive <em>from</em> the external source. Suppose <code>findAllProducts</code> receives the next JSON from the API:</p><pre><code>[
  {
    "id": "prod_12345",
    "name": "Wireless Headphones",
    "description": "Noise-cancelling over-ear Bluetooth headphones",
    "price": 149.99,
    "currency": "USD",
    // other properties...
  }
]</code></pre><p>So the payload type is the following:</p><pre><code>export type ProductPayload = {
  id: string;
  name: string;
  description: string;
  price: number;
  currency: string;
  // other properties...
};</code></pre><p>Of course, it should be an array in our case: <code>ProductPayload[]</code></p><h3>Data Transfer Objects (DTOs)</h3><p>Interfaces or types that describe the structure for the untyped data (like JSON) sent <em>to</em> the external source.</p><p>Following the example from the previous section, we can specify a DTO type for the query parameters of the <code>findAllProducts</code> method and call it <code>FindAllProductsDTO</code>. We&#8217;ll assume it contains one parameter called <code>category</code>, so the whole DTO type will look like:</p><pre><code>type FindAllProductsDTO = {
  category: string
}</code></pre><blockquote><p><strong>TIP:</strong> <a href="https://codewithstyle.info/typescript-dto/">Find out more about DTOs in this amazing article</a> by Mi&#322;osz Piechocki.</p></blockquote><h3>Module Type</h3><p>The main type of our module. It holds all the properties that will be displayed on the UI or used for business logic within the module.</p><p>Let&#8217;s say we need to display the data that <code>findAllProducts</code> returns us. For that, we may want to merge the <code>price</code> and <code>currency</code> of <code>ProductPayload</code> into a single <code>price</code> property. So the <code>Product</code> module type will look like this:</p><pre><code>type Product = {
  id: string;
  name: string;
  description: string;
  price: string;
  // other properties...
}</code></pre><p>With this structure, we can conveniently display all the product info on the UI.</p><h3>Prop types</h3><p>Simple type objects for React components (widgets and orchestrating component in our case) that we all know.</p><blockquote><p><strong>TIP:</strong> Follow the next sections to learn what widgets and orchestrating component are.</p></blockquote><p><a href="https://open.substack.com/pub/techwood/p/react-mvvm-model-layer?r=5uvuls&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=false">Learn more about the Model layer.</a></p><h2>View layer</h2><p>This layer displays the data and serves as a primary point of user interactions. The layer is divided into widgets - individual components that render the UI and respond to events. In most cases, all the widgets must be pure React components without any business logic inside and with a single return of JSX.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Nhkz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73e0798-9fd4-462f-8b94-d1cb2b48ce9c_603x303.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Nhkz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73e0798-9fd4-462f-8b94-d1cb2b48ce9c_603x303.png 424w, https://substackcdn.com/image/fetch/$s_!Nhkz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73e0798-9fd4-462f-8b94-d1cb2b48ce9c_603x303.png 848w, https://substackcdn.com/image/fetch/$s_!Nhkz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73e0798-9fd4-462f-8b94-d1cb2b48ce9c_603x303.png 1272w, https://substackcdn.com/image/fetch/$s_!Nhkz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73e0798-9fd4-462f-8b94-d1cb2b48ce9c_603x303.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Nhkz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73e0798-9fd4-462f-8b94-d1cb2b48ce9c_603x303.png" width="217" height="109.03980099502488" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f73e0798-9fd4-462f-8b94-d1cb2b48ce9c_603x303.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:303,&quot;width&quot;:603,&quot;resizeWidth&quot;:217,&quot;bytes&quot;:29404,&quot;alt&quot;:&quot;Illustration of the View Layer in MVVM architecture. A pink box labeled &#8220;View Layer&#8221; contains a magenta block labeled &#8220;Widgets.&#8221;&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;:&quot;https://techwood.substack.com/i/171201660?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73e0798-9fd4-462f-8b94-d1cb2b48ce9c_603x303.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Illustration of the View Layer in MVVM architecture. A pink box labeled &#8220;View Layer&#8221; contains a magenta block labeled &#8220;Widgets.&#8221;" title="Illustration of the View Layer in MVVM architecture. A pink box labeled &#8220;View Layer&#8221; contains a magenta block labeled &#8220;Widgets.&#8221;" srcset="https://substackcdn.com/image/fetch/$s_!Nhkz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73e0798-9fd4-462f-8b94-d1cb2b48ce9c_603x303.png 424w, https://substackcdn.com/image/fetch/$s_!Nhkz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73e0798-9fd4-462f-8b94-d1cb2b48ce9c_603x303.png 848w, https://substackcdn.com/image/fetch/$s_!Nhkz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73e0798-9fd4-462f-8b94-d1cb2b48ce9c_603x303.png 1272w, https://substackcdn.com/image/fetch/$s_!Nhkz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff73e0798-9fd4-462f-8b94-d1cb2b48ce9c_603x303.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption"><em>View layer is what users see and interact with.</em></figcaption></figure></div><p>Assume we want to display our list of products on the Home page. We&#8217;d create a widget called <code>ProductsList.tsx</code>, and one of the props for it would be an array of products - <code>Product[]</code>. Later, we can iterate through the array and display the products like a list of cards.</p><p>Learn more about the View layer.</p><h2>Application Layer</h2><p>Now that we can receive and display typed data, it&#8217;s time to handle business logic (use cases, if you prefer), and that&#8217;s where the Application layer comes in.</p><p>Some of the common tasks for this layer may include:</p><ul><li><p>Map the data before sending it to the View layer</p></li><li><p>Make conditional requests to the Data layer - decide <em>when</em> and <em>why</em> to fetch the data.</p></li><li><p>Initialize functions that will handle user interactions: clicks on buttons, filling a form, scrolling, etc.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AXQG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656bc0df-6bf1-4864-9cf3-26ac655e4353_603x303.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AXQG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656bc0df-6bf1-4864-9cf3-26ac655e4353_603x303.png 424w, https://substackcdn.com/image/fetch/$s_!AXQG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656bc0df-6bf1-4864-9cf3-26ac655e4353_603x303.png 848w, https://substackcdn.com/image/fetch/$s_!AXQG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656bc0df-6bf1-4864-9cf3-26ac655e4353_603x303.png 1272w, https://substackcdn.com/image/fetch/$s_!AXQG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656bc0df-6bf1-4864-9cf3-26ac655e4353_603x303.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AXQG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656bc0df-6bf1-4864-9cf3-26ac655e4353_603x303.png" width="221" height="111.04975124378109" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/656bc0df-6bf1-4864-9cf3-26ac655e4353_603x303.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:303,&quot;width&quot;:603,&quot;resizeWidth&quot;:221,&quot;bytes&quot;:31288,&quot;alt&quot;:&quot;Illustration of the Application Layer in MVVM architecture. A light green box labeled &#8220;Application Layer&#8221; contains a darker green block labeled &#8220;Hooks.&#8221;&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;:&quot;https://techwood.substack.com/i/171201660?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656bc0df-6bf1-4864-9cf3-26ac655e4353_603x303.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Illustration of the Application Layer in MVVM architecture. A light green box labeled &#8220;Application Layer&#8221; contains a darker green block labeled &#8220;Hooks.&#8221;" title="Illustration of the Application Layer in MVVM architecture. A light green box labeled &#8220;Application Layer&#8221; contains a darker green block labeled &#8220;Hooks.&#8221;" srcset="https://substackcdn.com/image/fetch/$s_!AXQG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656bc0df-6bf1-4864-9cf3-26ac655e4353_603x303.png 424w, https://substackcdn.com/image/fetch/$s_!AXQG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656bc0df-6bf1-4864-9cf3-26ac655e4353_603x303.png 848w, https://substackcdn.com/image/fetch/$s_!AXQG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656bc0df-6bf1-4864-9cf3-26ac655e4353_603x303.png 1272w, https://substackcdn.com/image/fetch/$s_!AXQG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656bc0df-6bf1-4864-9cf3-26ac655e4353_603x303.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption"><em>Hooks handle all the business logic across the module.</em></figcaption></figure></div><p>In React, I prefer to implement the Application layer as a group of hooks. Usually, each hook corresponds to a specific widget from the View layer, although sometimes we can pass the data from a single hook to multiple widgets. For example, for <code>ProductsList.tsx</code>, we would create a hook called <code>useProductsList.tsx</code>. This hook would handle all the business logic for the widget.</p><p>Learn more about the Application layer.</p><h2>Orchestrating component</h2><p>Once our business logic is isolated, it&#8217;s time to glue everything together. This is exactly the role of the Orchestrator.</p><p>You can probably guess from the name that the Orchestrating component connects all four layers and makes them work together. We need the Orchestrator since we can&#8217;t simply call hooks from the Application layer into View widgets, because then widgets won&#8217;t be clean components anymore.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eYqB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedd259be-b479-44b8-8515-8645c186fccf_723x1833.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eYqB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedd259be-b479-44b8-8515-8645c186fccf_723x1833.png 424w, https://substackcdn.com/image/fetch/$s_!eYqB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedd259be-b479-44b8-8515-8645c186fccf_723x1833.png 848w, https://substackcdn.com/image/fetch/$s_!eYqB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedd259be-b479-44b8-8515-8645c186fccf_723x1833.png 1272w, https://substackcdn.com/image/fetch/$s_!eYqB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedd259be-b479-44b8-8515-8645c186fccf_723x1833.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eYqB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedd259be-b479-44b8-8515-8645c186fccf_723x1833.png" width="253" height="641.4232365145228" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/edd259be-b479-44b8-8515-8645c186fccf_723x1833.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1833,&quot;width&quot;:723,&quot;resizeWidth&quot;:253,&quot;bytes&quot;:194628,&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;:&quot;https://techwood.substack.com/i/171201660?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedd259be-b479-44b8-8515-8645c186fccf_723x1833.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!eYqB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedd259be-b479-44b8-8515-8645c186fccf_723x1833.png 424w, https://substackcdn.com/image/fetch/$s_!eYqB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedd259be-b479-44b8-8515-8645c186fccf_723x1833.png 848w, https://substackcdn.com/image/fetch/$s_!eYqB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedd259be-b479-44b8-8515-8645c186fccf_723x1833.png 1272w, https://substackcdn.com/image/fetch/$s_!eYqB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fedd259be-b479-44b8-8515-8645c186fccf_723x1833.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><figcaption class="image-caption"><em>Orchestrator groups four layers together.</em></figcaption></figure></div><p>If we had a Home page that should show a list of products, the Orchestrator would first call hooks from the Application layer. Then, it would pass the data returned from each hook to the appropriate widget in the View layer, and finally return those widgets as JSX.</p><pre><code>import { useProductsList } from './application/useProductsList';
import { ProductsList } from './view/ProductsList';
import { Loading } from '~/components/Loading';
import { Error } from '~/components/Error';

export const Home = () =&gt; {
  const { products, isLoading, error } = useProductsList();

  // You can handle loading and error state in the Orchestrator or in each widget individually 
  if (isLoading) return &lt;Loading /&gt;;
  if (error) return &lt;Error /&gt;;

  return &lt;ProductsList products={products} /&gt;;
};</code></pre><p>Learn more about Orchestrator.</p><h2>Disadvantages</h2><p>As with everything in the world, this architecture is not perfect either. Here are some downsides that I&#8217;ve spotted while I was implementing it on a variety of projects:</p><ul><li><p><strong>Adoption</strong> - when you try to implement this architecture on the existing project, you may face rejection from your colleagues, claiming that it&#8217;ll bring complexity and unnecessary boilerplate to the codebase. In this situation, don&#8217;t rush refactoring the whole project. Start slow, implement new or refactor existing small module using MVVM. Document the real benefits and show them to the doubters.</p></li><li><p><strong>More boilerplate</strong> - yes, MVVM will bring more boilerplate, but you can automate its creation using tools like <a href="https://plopjs.com/">plop.js</a> and stop wasting time. Eventually, you will even win from having a standardized structure that you can bootstrap automatically, as it will bring order to the project.</p></li></ul><h2>Conclusion</h2><p>Congratulations! &#127881;</p><p>You&#8217;ve learned the fundamentals of MVVM and its practical <em>five-layer implementation</em>. We&#8217;ve discussed how to CRUD the data, type it, apply the business logic, and present it on the UI.</p><p>Want to go deeper? Use the links at the end of each section, or start experimenting right away using this high-level overview I&#8217;ve provided in the article.</p><div><hr></div><p>Creating content like this is not an easy ride, so I hope you&#8217;ll hit that like button and drop a comment. As usual, constructive feedback is always welcome!</p><p>&#128279; <a href="https://www.linkedin.com/in/olexandr-pylypyshyn/">Follow me on LinkedIn</a></p><p>&#128240; Read me on the platform of your choice: <a href="https://techwood.substack.com/">Substack</a>, <a href="https://dev.to/techwood">DEV.to</a></p><p>Till next time! &#128075;</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://techwood.substack.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 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>]]></content:encoded></item></channel></rss>