<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Jobber</title>
    <description>The latest articles on Forem by Jobber (@jobber).</description>
    <link>https://forem.com/jobber</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F5029%2Fe552119d-35ff-44de-8bab-550f1497d317.png</url>
      <title>Forem: Jobber</title>
      <link>https://forem.com/jobber</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/jobber"/>
    <language>en</language>
    <item>
      <title>Removing Friction: How AI Tools Streamline Our Engineering Workflows</title>
      <dc:creator>Sophie Dubois</dc:creator>
      <pubDate>Tue, 25 Nov 2025 19:58:01 +0000</pubDate>
      <link>https://forem.com/jobber/removing-friction-how-ai-tools-streamline-our-engineering-workflows-244g</link>
      <guid>https://forem.com/jobber/removing-friction-how-ai-tools-streamline-our-engineering-workflows-244g</guid>
      <description>&lt;p&gt;Hi! I’m Sophie, a Software Engineer on the Network Venture team at Jobber. Our team spends a lot of time obsessing over ways to make Jobber simpler and easier for our customers to use. We try to remove unnecessary clicks, reduce decision fatigue, and help users get to value faster. Every week, we ask ourselves: &lt;em&gt;“Where’s the friction? How do we clear it away?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It makes sense, then, that the biggest change in how our engineering team works lately comes from applying that same philosophy to ourselves. Our developers have started using AI-powered tools to streamline how we write and test code. Tasks that used to involve half a dozen tabs now happen directly in our code editor. In short, AI is doing for us what we’ve always tried to do for our customers: cutting out the friction.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Enemy: Context Switching
&lt;/h3&gt;

&lt;p&gt;Historically, building even a small feature required juggling an overwhelming number of tools. Let’s paint a picture, shall we? We want to experiment with a new widget on our home page. Seems straightforward enough, right?&lt;/p&gt;

&lt;p&gt;We start in Jira, where the feature requirements live, and gather all the context we need.&lt;/p&gt;

&lt;p&gt;We’re not totally sure what the best practices are for this type of experimentation, so we switch over to Confluence and do a bit of digging through our docs. &lt;/p&gt;

&lt;p&gt;Once we feel confident with our approach, we hop into our code editor to get started.&lt;/p&gt;

&lt;p&gt;Halfway through, a pesky bug pops up that we can’t quite pin down. Time to jump out to Stack Overflow or Google.&lt;/p&gt;

&lt;p&gt;A few minutes later, we realize we’ve forgotten a small but important detail from the requirements, &lt;em&gt;“what’s the exact copy for the button again?”.&lt;/em&gt; Back to Jira we go.&lt;/p&gt;

&lt;p&gt;By the time we’ve bounced between tickets, docs, forums, and our editor, the actual “building” feels like just one part of a much bigger juggling act.&lt;/p&gt;

&lt;p&gt;If only there were a way to stay focused in one place, while still having all the context and answers at our fingertips...&lt;/p&gt;

&lt;h3&gt;
  
  
  Cursor to the Rescue
&lt;/h3&gt;

&lt;p&gt;Cursor is an AI-powered code editor that keeps us in the flow. We can write code, debug issues, and pull in information without ever leaving the editor. We’ve been really excited about the efficiency gains we get with this tool so we’ve doubled down on making it even more useful by writing Jobber-specific Cursor rules and connecting it to our own Jobber MCP server. &lt;/p&gt;

&lt;p&gt;Cursor rules are directions we give to Cursor with our own best practices baked in. Instead of just asking AI to “build a GraphQL mutation” or “write a test,” we can write rules that tell Cursor exactly how we expect those things to look at Jobber. The result is code skeletons and patterns that already follow our conventions.&lt;/p&gt;

&lt;p&gt;Our MCP server takes it one step further, allowing us to hook Cursor directly into our Atlassian environment. That means all the best-practice documentation from Confluence and the project requirements in Jira tickets are now just a prompt away, right inside the editor. In practice, that means less time hunting for answers and more time building!&lt;/p&gt;

&lt;h3&gt;
  
  
  The Impact
&lt;/h3&gt;

&lt;p&gt;Let’s revisit that same widget experiment scenario, but now with Cursor in the mix. &lt;/p&gt;

&lt;p&gt;We kick things off right inside Cursor. The requirements from Jira are pulled straight into the editor with a single prompt, and we can double-check our approach by pulling in best practices from Confluence — right there beside our code.&lt;/p&gt;

&lt;p&gt;When a bug shows up, we just drop the error into the Cursor chat and troubleshoot it together. No more bouncing between tools. Just steady, focused building.&lt;/p&gt;

&lt;p&gt;Since we’re already in flow, why stop there? We can ask Cursor to spin up some specs for us. We have a Cursor rule for that. And while we’re at it, let’s have Cursor draft the pull request. There’s a rule for that too!&lt;/p&gt;

&lt;p&gt;But the real impact goes beyond saving a few clicks. For engineers, it means more focus time and less mental overhead. For teams, it means less wheel spinning, faster iteration cycles, and more consistent code quality because best practices are baked right into the workflow. The small efficiencies compound, allowing us to experiment quickly, learn fast, and adapt. &lt;/p&gt;

&lt;p&gt;The irony isn’t lost on us. As a Growth team, we focus on reducing friction for Jobber customers, and now with AI, we’re doing the same for our own workflows. In other words, we’re not just practicing what we preach, we’re coding the way we want our customers to work: simply, seamlessly, and without friction.&lt;/p&gt;

&lt;h4&gt;
  
  
  About Jobber
&lt;/h4&gt;

&lt;p&gt;Our awesome Jobber technology teams span across Payments, Infrastructure, AI/ML, Business Workflows &amp;amp; Communications. We work on cutting edge &amp;amp; modern tech stacks using React, React Native, Ruby on Rails, &amp;amp; GraphQL. &lt;/p&gt;

&lt;p&gt;If you want to be a part of a collaborative work culture, help small home service businesses scale and create a positive impact on our communities, then visit our &lt;a href="https://www.getjobber.com/about/careers/?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=eng_blog" rel="noopener noreferrer"&gt;careers&lt;/a&gt; site to learn more!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Designing simple, powerful lists 💪</title>
      <dc:creator>Laura Erickson</dc:creator>
      <pubDate>Thu, 07 Mar 2024 18:45:25 +0000</pubDate>
      <link>https://forem.com/jobber/designing-simple-powerful-lists-4g3m</link>
      <guid>https://forem.com/jobber/designing-simple-powerful-lists-4g3m</guid>
      <description>&lt;p&gt;At Jobber, we design and build products that contain everything field-service businesses need to run their operations effectively. With so much information to manage in our interface, it’s critical to layer data on the right surface so our customers can quickly find the information they need, and act on it to run their businesses as efficiently as possible.&lt;/p&gt;

&lt;p&gt;We found that as our customers' businesses grew, they were outgrowing the functionality we were providing around maximizing productivity 🌱. Over time, lists in our product had been implemented with inconsistent patterns, they required navigating back and forth to complete basic actions; and were visually very busy. &lt;/p&gt;

&lt;p&gt;We started identifying commonalities across these lists and discovered many opportunities to standardize the content hierarchy, streamline repetitive tasks with row-level actions, and simplify sorts and filters. We began rebuilding with goals to: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reduce visual clutter;&lt;/li&gt;
&lt;li&gt;create consistent intuitive patterns; and&lt;/li&gt;
&lt;li&gt;⚡ supercharge jobs-to-be-done;&lt;/li&gt;
&lt;li&gt;all while improving performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcuhs30xvll5esm6z3y4e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcuhs30xvll5esm6z3y4e.png" alt="Image description" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We wanted to get to a high level of confidence in our solution before building it into the product, so when we landed on an iteration we felt good about, we developed a research plan and a fully interactive prototype to share with customers. With some adjustments, we were able to validate our simplified UI and decided to stress-test it with our customers. We built and released the first list into production for a small number of users, adjusting and expanding as we felt was necessary.&lt;/p&gt;

&lt;p&gt;By giving our customers the right information and allowing them to act on it, we prevented having to navigate back and forth from their client list over 15,000 times just in the first month. Whenever you hear “Happy customer!!!” feedback, you know you’re on the right track.&lt;/p&gt;

&lt;p&gt;This initiative is a great example of user-centric, design-led product development, and is one of many significant improvements we’ll be making to the product experience this year. I’m ready to push boundaries and drive towards excellence for our users 🏎. Now: onto the next simple, powerful thing!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About Jobber&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our awesome Jobber technology teams span across Payments, Infrastructure, AI/ML, Business Workflows &amp;amp; Communications. We work on cutting edge &amp;amp; modern tech stacks using React, React Native, Ruby on Rails, &amp;amp; GraphQL. &lt;/p&gt;

&lt;p&gt;If you want to be a part of a collaborative work culture, help small home service businesses scale and create a positive impact on our communities, then visit our &lt;a href="https://getjobber.com/about/careers?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=eng_blog"&gt;careers&lt;/a&gt; site to learn more!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Behind the scenes with Jobber’s Dev Acceleration team</title>
      <dc:creator>Tim van der Kooi</dc:creator>
      <pubDate>Wed, 17 Jan 2024 22:10:48 +0000</pubDate>
      <link>https://forem.com/jobber/behind-the-scenes-with-jobbers-dev-acceleration-team-265l</link>
      <guid>https://forem.com/jobber/behind-the-scenes-with-jobbers-dev-acceleration-team-265l</guid>
      <description>&lt;p&gt;Many software companies love to obsess about customer obsession. That’s no different at Jobber: our teams are honed in on providing an easy-to-use experience for service professionals so they can focus on providing value to their customers.&lt;/p&gt;

&lt;p&gt;But what about developer obsession? How can we do the same thing for our developers and make it easier for them to deliver value to service professionals?&lt;/p&gt;

&lt;p&gt;At Jobber, we decided to tackle that problem with a new team called Dev Acceleration. The goal of this team is to address the bottlenecks and pain points that hinder development productivity and reduce friction with suboptimal tools or processes.&lt;/p&gt;

&lt;p&gt;This type of team can go by many different names: dev acceleration, developer experience, or even engineering enablement. The norms around naming this team isn’t commonplace at many software companies, but the core idea is spreading. &lt;/p&gt;

&lt;p&gt;This post will take a behind-the-scenes look at what it’s been like to establish the Dev Acceleration team at Jobber.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know your developers and establish your mission
&lt;/h2&gt;

&lt;p&gt;First and foremost, a Dev Acceleration team has to dig into developer pain points and the status quo without initial bias. They must know and listen to their customer: namely, developers. This is no different than any other feature team!&lt;/p&gt;

&lt;p&gt;It’s important to come into this role with opinions that are loosely held: you can have a preconceived notion of what needs to be done, but in order to make progress, it comes down to establishing a mission and establishing relationships with engineers who can guide the team in the direction of that mission. &lt;/p&gt;

&lt;p&gt;How that is achieved at one company can look extremely different at another!&lt;/p&gt;

&lt;p&gt;I’ve worked with companies that had many different teams managing their own infrastructure and operated their own microservices with Kubernetes. There was a high demand for a platform engineering team that could provide services for teams to manage their own services independently. Jobber, on the other hand, has a centralized infrastructure team hosting a Rails monolith that most of our engineering teams work on simultaneously. Clearly, what engineers desired in my past experience may not be something that Jobber engineers want, need, or even think about!&lt;/p&gt;

&lt;p&gt;When I started at Jobber, my manager had compiled a 12-page document exploring pain points within engineering. This was a great starting point to focus my energy and talk to people within engineering about the most pressing issues. Over the course of a few weeks, I chatted with many engineering managers and principal engineers to confirm the pain points in that document, and from there generated a developer survey to get feedback from the rest of the engineers in the company about their experience.&lt;/p&gt;

&lt;p&gt;From the survey responses we received, I felt it was prudent to follow up with developers who showed some passion about the bottlenecks at the company. This usually comes from senior engineers or tech leads, but it’s important to note junior engineers' opinions as well. Seniors may be skilled enough to work around some of the challenges, but they can overlook some of them. Juniors might struggle with these hidden problems. By engaging with engineers of all kinds and encouraging them to follow through on these problems, we foster that culture that generates great quality-of-life pull requests from engineers that want to make Jobber an easier habitat to get things done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Establishing a Developer Acceleration mission
&lt;/h2&gt;

&lt;p&gt;A key question for any developer experience team to consider is: How are you actually improving the developer experience or productivity at the company? What does developer experience even mean?&lt;/p&gt;

&lt;p&gt;Answering this question will get your team to start thinking about its mission and your metrics. For us, our mission at Jobber is: &lt;strong&gt;we enable engineers to ship faster&lt;/strong&gt;. That means we’re focused on producing four types of value for developers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed&lt;/strong&gt;: we remove blockers that are slowing devs from shipping code faster&lt;br&gt;
&lt;strong&gt;Efficiency&lt;/strong&gt;: we reduce developer toil and build our tooling to scale&lt;br&gt;
&lt;strong&gt;Clarity&lt;/strong&gt;: we reduce cognitive load and make processes easier&lt;br&gt;
&lt;strong&gt;Autonomy&lt;/strong&gt;: we reduce dependencies and make tooling that is self-serve&lt;/p&gt;

&lt;p&gt;These values serve as the benchmark for where we want to go. From these values, we can determine metrics that prove that we’re having an impact in our target value area.&lt;/p&gt;

&lt;p&gt;In the future, we want to leverage frameworks like DORA or SPACE to provide quantitative and qualitative data for our work. But for the time being, we’ve kept our focus on honing our metrics to more targeted areas within our control.&lt;/p&gt;

&lt;p&gt;It’s likely that some of the work will be a lagging indicator. You might not see results right away. Be patient: Adoption of your new, improved tooling and processes will take time. &lt;/p&gt;

&lt;h2&gt;
  
  
  Exploring themes and bottlenecks
&lt;/h2&gt;

&lt;p&gt;With the soul-searching complete, it was time to dial in on the low-hanging fruit that could provide the most impact. We knew there were great opportunities to improve the speed at which things are done at Jobber. But we had to be mindful that we didn’t spend a lot of time doing the wrong thing to speed things up based on loose hypotheses. Personally, I think one of the worst approaches a team like this could take would be to take on a large and lengthy project filled with uncertainty and ultimately fail to deliver results - or even worse, results that no one wants!&lt;/p&gt;

&lt;p&gt;That was almost the case with our first project, and why we live by taking small bites and working iteratively.&lt;/p&gt;

&lt;h3&gt;
  
  
  Improving local dev environments
&lt;/h3&gt;

&lt;p&gt;Something that was addressed by many engineers in the company was difficulty getting their dev environment set up and keeping it up-to-date. Our setup can be a bit tedious, and it’s not uncommon for a tooling/language update to happen in our codebase which can break a few developers’ environments if they miss the public service announcement.&lt;/p&gt;

&lt;p&gt;To address this, we decided to explore GitHub Codespaces and dev containers as a way to automate the dev environment and provide on-demand testing environments for engineers, product managers, and engineering managers to access. It was actually pretty easy to get Jobber running in Codespaces in a couple of days! But could it be a full-on replacement for dev environments? Would it support all of our integrations? Would it be stable enough?&lt;/p&gt;

&lt;p&gt;We delivered a proof-of-concept, a trial run with a handful of devs, and further investigation that revealed the Codespaces experience was too tedious and required much more effort and automation to bring up to parity with a local dev environment. Ultimately, we decided that it was not worth pursuing further and would revisit in the future.&lt;/p&gt;

&lt;p&gt;But this was a great example of pivoting quickly: we placed a small bet, got some feedback, and decided it wasn’t the best use of our time to bullet-proof a solution there. With a small team of three engineers, there were even bigger wins to be had with that involved less uncertainty.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI/CD pipeline
&lt;/h3&gt;

&lt;p&gt;The performance of the CI/CD pipeline affects every single Jobber engineer. Whether it’s a flaky test, a blocked deployment pipeline, or long-running test suites that get slower by the day, the impact of a poorly running CI/CD pipeline can silently grow and is most likely to be accepted as ‘that’s the way it is’. At Jobber, there was some degree of that but there also wasn’t a dedicated team to tackle those sorts of issues with the time needed to dig deep.&lt;/p&gt;

&lt;p&gt;As a result, a majority of our time has been spent improving the performance of that pipeline and stabilizing that process for the future growth of more Jobber engineers to build, test, and deploy their changes. &lt;/p&gt;

&lt;p&gt;Our biggest win so far has been reducing the pipeline time by nearly 50%. Our test times were sometimes hitting 20 mins per run for 30,000 rspec tests and 6,000 Jest tests. With some work towards more efficient usage of the testing infrastructure, we’ve brought our times down to 12 mins per run with 40,000 rspec tests and 10,000 Jest tests for roughly the same cost per run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prioritizing the most impactful work
&lt;/h3&gt;

&lt;p&gt;We could have put all of our effort into making GitHub Codespaces our dev environment of choice, but was that guaranteed to be useful, adopted, and loved by our engineers? Tackling the low effort/high impact items may not be flashy, but when you’re a small team just starting out, it’s important to establish trust and deliver meaningful results.&lt;/p&gt;

&lt;p&gt;What this looks like at another company will probably be different. A developer experience team will likely have a different focus based upon their developer discovery work. But it’s important that you’re not led astray by recency bias, where the last thing that bothered you seems like the priority. A Dev Acceleration team has to discern the high impact work that will affect the most people.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where we’re headed next
&lt;/h2&gt;

&lt;p&gt;As mentioned earlier, Jobber is a monolith and the majority of our teams contribute code to the same repository daily. As we scale, we want to ensure the values we espouse scales with the monolith, too. We don’t want to slow down just because we are growing.&lt;/p&gt;

&lt;p&gt;That means maintaining speed and efficiency: we want to build our pipeline to scale for fast builds, test suites, and deploys. We average around 40-50 commits to our repository a day and we want to build a release pipeline that scales for releasing 100s of times a day.&lt;/p&gt;

&lt;p&gt;We also want to improve autonomy and clarity. Establishing ownership over components of the codebase is a critical piece of the puzzle. We want to improve our tooling for staking ownership within the monolith and have that propagate into our observability and processes. This will make it easier to scale our day-to-day operations of the monolith.&lt;/p&gt;

&lt;p&gt;And most importantly, we want to hear what devs have to say about their current experience. That means crafting a developer survey to hear their pain points and allow us to establish quantitative and qualitative metrics to see if we’re working on the right things, and making it easier to ship code faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hiring for a Dev Acceleration team
&lt;/h2&gt;

&lt;p&gt;Maybe after all this discussion, you’re starting to think about what it would be like to put together a team of your own to tackle the large, festering issues at your company. It’s tempting to just put together a Dev Acceleration team from your pool of existing developers. After all, they should know their problems best. But then you might be taking away your best developers from providing great features to the product!&lt;/p&gt;

&lt;p&gt;I believe a mix of tenured engineers, and new, but experienced engineers is the best fit. The tenured folk have a great understanding of the day-to-day pain points of development, while the new hires come in with a fresh perspective. They offer a way of tackling problems that may be accepted as status quo, and can also distill best practices from their previous experiences.&lt;/p&gt;

&lt;p&gt;It’s also beneficial to have developers of different types and backgrounds. Not everyone needs to be an expert in your existing tech stack. &lt;/p&gt;

&lt;p&gt;For example, it’s completely reasonable to have some developers who are proficient in the stack paired with some site reliability engineers (SRE) or DevOps engineers. Together they can cover a large breadth of the work that comes with a Dev Acceleration team. Similar to having a mix of tenured and new developers, a blend of expertise offers the benefit of having many different perspectives for solving any challenge that might come up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dev Acceleration is an iterative process
&lt;/h2&gt;

&lt;p&gt;Dev Acceleration teams are forging a fairly new path. &lt;/p&gt;

&lt;p&gt;This practice has been established at quite a few larger companies. It’s now starting to make its way into growing startups that are looking to scale up their engineering department. &lt;/p&gt;

&lt;p&gt;There are not a lot of industry standards around how developer experience teams operate and perform. It’s a lot like when DevOps was first becoming a thing.&lt;/p&gt;

&lt;p&gt;We know we’re going to make mistakes, but we do this by making small bets and getting feedback quickly. Dev Acceleration teams really benefit from feedback loops. Software is built upon iteration, and Dev Acceleration is no exception. It will take investigation, experimentation, and being flexible to figure out what works.&lt;/p&gt;

&lt;p&gt;Dev Acceleration teams will have the main benefit of working so closely with their customers. It’s a rewarding experience and a privilege to see and interact with the devs who are benefitting from your work on a day-to-day basis.&lt;/p&gt;

&lt;h2&gt;
  
  
  About Jobber
&lt;/h2&gt;

&lt;p&gt;Our awesome Jobber technology teams span across Payments, Infrastructure, AI/ML, Business Workflows &amp;amp; Communications. We work on cutting edge &amp;amp; modern tech stacks using React, React Native, Ruby on Rails, &amp;amp; GraphQL. &lt;/p&gt;

&lt;p&gt;If you want to be a part of a collaborative work culture, help small home service businesses scale and create a positive impact on our communities, then visit our careers site to learn more!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>devacceleration</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building an App in Jobber Platform</title>
      <dc:creator>Christian Ellinger</dc:creator>
      <pubDate>Thu, 14 Dec 2023 17:33:07 +0000</pubDate>
      <link>https://forem.com/jobber/building-an-app-in-jobber-platform-5259</link>
      <guid>https://forem.com/jobber/building-an-app-in-jobber-platform-5259</guid>
      <description>&lt;h2&gt;
  
  
  Building the application
&lt;/h2&gt;

&lt;p&gt;We are back with the second post of this three-part series. In the &lt;a href="https://dev.to/jobber/building-an-app-in-jobber-platform-57aj"&gt;first post&lt;/a&gt; we talked about Jobber’s objectives for rebuilding the QBO integration, the principles behind it and which architectural decisions the team had to make while building the integration. The Jobber platform through the Developer Center was introduced and in this post we will dig deeper into the details which entail building the application using &lt;a href="https://developer.getjobber.com/landing" rel="noopener noreferrer"&gt;Jobber’s Developer Center&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting started
&lt;/h3&gt;

&lt;p&gt;To get started with building applications in Jobber’s platform, there are a few pre-requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a &lt;a href="https://getjobber.com/developer-sign-up/" rel="noopener noreferrer"&gt;developer account&lt;/a&gt; in Jobber;&lt;/li&gt;
&lt;li&gt;Create a &lt;a href="https://developer.getjobber.com/signup/" rel="noopener noreferrer"&gt;Developer Center account&lt;/a&gt;. (It is important to note that the Jobber developer account is a completely separate account from the Developer Center account);&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Creating the app
&lt;/h3&gt;

&lt;p&gt;After creating the developer’s accounts, head over to your &lt;a href="https://developer.getjobber.com/apps" rel="noopener noreferrer"&gt;apps&lt;/a&gt; page and click on the 'NEW' button to create your first app.&lt;/p&gt;

&lt;p&gt;When creating your application, you will be prompted for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App name&lt;/strong&gt; (required)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer name&lt;/strong&gt; (required)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth Callback URL&lt;/strong&gt; (optional)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manage App URL&lt;/strong&gt; (optional)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App description&lt;/strong&gt; (required)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Features &amp;amp; benefits&lt;/strong&gt; (optional)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scopes&lt;/strong&gt; (required)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App logo&lt;/strong&gt; (optional)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gallery images&lt;/strong&gt; (optional)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks&lt;/strong&gt; (optional)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any details about this app such as App Name or Description can be modified later, but it is important to select the correct scopes when starting as these currently cannot be edited later (a new app will need to be created instead). &lt;strong&gt;App name&lt;/strong&gt;, &lt;strong&gt;Developer name&lt;/strong&gt;, &lt;strong&gt;App description&lt;/strong&gt;, &lt;strong&gt;Features &amp;amp; benefits&lt;/strong&gt;, &lt;strong&gt;App logo&lt;/strong&gt;, and &lt;strong&gt;Gallery images&lt;/strong&gt; are all important pieces that'll make up the content of your app listing in Jobber's App Marketplace.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;App Name&lt;/strong&gt;, &lt;strong&gt;Developer name&lt;/strong&gt;, &lt;strong&gt;App Description&lt;/strong&gt;, &lt;strong&gt;Manage app URL&lt;/strong&gt;, and &lt;strong&gt;Features or benefits&lt;/strong&gt; will be used by your app's listing on the marketplace (if you choose to publish your app). Check the &lt;a href="https://developer.getjobber.com/docs/build_with_jobber/manage_apps/#publishing-your-application-on-the-marketplace" rel="noopener noreferrer"&gt;Publishing Your Application&lt;/a&gt; section for more details.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;OAuth Callback URL&lt;/strong&gt; is the URL that a Jobber user will be redirected to immediately after connecting and authorizing access to your app.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Manage App URL&lt;/strong&gt; is an alternative URL that can be used for any accounts that have already connected to your app but will have a need to navigate back to it to manage or configure functionalities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scopes&lt;/strong&gt; are where you can set exactly what kinds of data your app will be able to read or write from Jobber accounts using the GraphQL API. They also determine exactly what is shown on the OAuth screen when a Jobber user is connecting to your app. While an app is in a Draft state, the scopes can be freely edited. However, if an app is published and would like to add more scopes, all accounts that have connected to the app previously will need to re-authorize the app. More details on editing an app can be found &lt;a href="https://developer.getjobber.com/docs/publishing_your_app/editing_a_published_app" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.getjobber.com/docs/using_jobbers_api/setting_up_webhooks/" rel="noopener noreferrer"&gt;Webhooks&lt;/a&gt;&lt;/strong&gt; allow for real-time data to be sent to your application when an event occurs on models within Jobber. Webhooks are configured at the application level and will trigger when an account with an installation performs the topic. As an example, if you've configured a webhook to listen to CLIENT_CREATE, whenever a user creates a new client, the URL provided will be sent a POST request with the details in the body.&lt;/p&gt;

&lt;p&gt;For more details on how Jobber apps work and the processes around implementing, testing, and submitting your app for review, see the guides in the &lt;a href="https://developer.getjobber.com/docs/build_with_jobber/app_lifecycle" rel="noopener noreferrer"&gt;Build With Jobber&lt;/a&gt; section. There is also an &lt;a href="https://developer.getjobber.com/docs/app_template_project" rel="noopener noreferrer"&gt;App Template Project&lt;/a&gt; section if you would like to download and set up a working template app to start with. It is a good way to quickly start the development using some of the pre-setup projects with UI components, authentication and API examples. In the next section, we will explore more about how the Authentication and GraphQL API works.&lt;/p&gt;

&lt;h4&gt;
  
  
  Authenticating the app
&lt;/h4&gt;

&lt;p&gt;Jobber uses &lt;a href="https://auth0.com/intro-to-iam/what-is-oauth-2" rel="noopener noreferrer"&gt;OAuth 2.0&lt;/a&gt; for app authorization. OAuth 2.0 is widely adopted across various platforms, including web, desktop, and mobile applications. It's used by major applications like Facebook and GitHub.&lt;/p&gt;

&lt;p&gt;Upon &lt;a href="https://developer.getjobber.com/apps" rel="noopener noreferrer"&gt;adding your app&lt;/a&gt;, Jobber provides a client identifier and secret. The client ID is public, and used for identification, while the secret is confidential for authenticating with the Jobber API.&lt;/p&gt;

&lt;p&gt;To authorize your app, an admin initiates the process via Jobber's App Marketplace or a provided link:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

https://api.getjobber.com/api/oauth/authorize?client_id=&amp;lt;CLIENT_ID&amp;gt;&amp;amp;redirect_uri=&amp;lt;REDIRECT_URL&amp;gt;&amp;amp;state=&amp;lt;STATE&amp;gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;CLIENT_ID&amp;gt;&lt;/code&gt;: Your app's Client ID from the Developer Center&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;CALLBACK_URL&amp;gt;&lt;/code&gt;: The URL where Jobber redirects after authorization.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;STATE&amp;gt;&lt;/code&gt;: A random string to prevent cross-site request forgery attacks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a user interacts with a link or clicks the Connect button in Jobber's App Marketplace, they will be asked to grant authorization to the app by clicking an "Allow Access" button. If the user is not already logged into their Jobber account, they will need to log in before reaching the authorization screen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp96072jma2hpjjk76jxc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp96072jma2hpjjk76jxc.png" alt="An authorization modal for the Jobber platform. The title reads "&gt;&lt;/a&gt;&lt;br&gt;
After approval, Jobber redirects the user to your app's callback URL with an authorization code:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

https://yourapplication.com/callback?code=AUTHORIZATION_CODE&amp;amp;state=STATE


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In case the user denies the access request, Jobber will redirect the user-agent to your application's redirect URI without any additional parameters.&lt;/p&gt;

&lt;p&gt;Your app then obtains an access token by making a POST request to Jobber's token endpoint. The token is used for API interactions.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

https://api.getjobber.com/api/oauth/token?client_id=&amp;lt;CLIENT_ID&amp;gt;&amp;amp;client_secret=&amp;lt;CLIENT_SECRET&amp;gt;&amp;amp;grant_type=authorization_code&amp;amp;code=&amp;lt;AUTHORIZATION_CODE&amp;gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If the access token expires, use the refresh token to obtain a new one. Refresh tokens may expire due to app disconnection, manual client secret rolling, re-authorization after scope change, or Refresh Token Rotation.&lt;/p&gt;

&lt;p&gt;If both the access and refresh tokens expire, the user will need to initiate the OAuth flow again. Your app should prompt them to do so by redirecting them to Jobber and back to the app's Callback URL.&lt;/p&gt;

&lt;p&gt;Visit our &lt;a href="https://developer.getjobber.com/docs/building_your_app/app_authorization/" rel="noopener noreferrer"&gt;Developer Center&lt;/a&gt; for detailed information on how the authorization works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using the GraphQL Playground
&lt;/h3&gt;

&lt;p&gt;Before fully building out your app you can view the GraphQL Docs and Schema and make API requests using the GraphQL Playground tool and allowing access to your tester Jobber account. To do this, click on the three dots next to your &lt;a href="https://developer.getjobber.com/apps" rel="noopener noreferrer"&gt;app&lt;/a&gt; in the Developer Center and then click Test in Playground.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxia03vpmpfzjh2w29hi2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxia03vpmpfzjh2w29hi2.png" alt="a dropdown modal with the options: " id=""&gt;&lt;/a&gt;&lt;br&gt;
This will take you through the same Oauth2 flow that a normal admin user would do, and afterwards you'll be redirected to GraphQL Playground where you can view the available queries and mutations with the DOCS button on the right, or you can download the entire Schema. Note that the scopes on your application will be respected and you may not be able to query for all of the data seen within our schema.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe6v24ffrsukz2t9p36th.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe6v24ffrsukz2t9p36th.png" alt="a screenshot of the graphql playground showing available queries in the jobber api schema"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An example query will already be provided for you, but a second example query you could use to view the &lt;strong&gt;id&lt;/strong&gt;, &lt;strong&gt;jobNumber&lt;/strong&gt;, and &lt;strong&gt;title&lt;/strong&gt; of every Job in your tester account is:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

query SampleQuery {
  jobs {
    nodes {
      id
      jobNumber
      title
    }
  }
}


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Alternatively, you can make API requests by posting your GraphQL query or mutation to &lt;strong&gt;&lt;a href="https://api.getjobber.com/api/graphql" rel="noopener noreferrer"&gt;https://api.getjobber.com/api/graphql&lt;/a&gt;&lt;/strong&gt;. You will need to include the Access token from step 4 under the &lt;strong&gt;Authorization&lt;/strong&gt; header, preceded by the word &lt;strong&gt;bearer&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other important things
&lt;/h2&gt;

&lt;h3&gt;
  
  
  App-configured custom fields
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://developer.getjobber.com/docs/using_jobbers_api/custom_fields/" rel="noopener noreferrer"&gt;Custom fields&lt;/a&gt; in Jobber are a popular feature, allowing various data formats to be linked to six Jobber objects: &lt;strong&gt;Clients&lt;/strong&gt;, &lt;strong&gt;Properties&lt;/strong&gt;, &lt;strong&gt;Quotes&lt;/strong&gt;, &lt;strong&gt;Jobs&lt;/strong&gt;, &lt;strong&gt;Invoices&lt;/strong&gt;, and soon &lt;strong&gt;Team members&lt;/strong&gt;. These fields are set up at the account level and store values at the object level. Admin users typically configure these fields, but the API also permits apps to create new custom fields with proper permissions.&lt;/p&gt;

&lt;p&gt;Custom field configurations include settings like &lt;strong&gt;custom field name&lt;/strong&gt;, &lt;strong&gt;automatic data transfer to related objects&lt;/strong&gt;, &lt;strong&gt;default values&lt;/strong&gt;, and &lt;strong&gt;data types&lt;/strong&gt; (True/False, Numeric, Area, Dropdown, Text, Link), along with &lt;strong&gt;units of measure&lt;/strong&gt; for numeric and area fields. &lt;/p&gt;

&lt;p&gt;When an app configures a custom field, its name and logo are displayed to Jobber users whenever they encounter the field in Jobber's user interface. This enhances the user experience and association with the app. For example, if an app called “XYZ App” configured two custom fields on all job objects, the resulting user experience would look like the screenshot below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Finuoy9v47sqhdtx5um9r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Finuoy9v47sqhdtx5um9r.png" alt="A screenshot of the Job view in Jobber. Two arrows point to a section of the job view that show the app configured custom fields. The arrows point to the app logo, and the app's name"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  API Versioning
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://developer.getjobber.com/docs/using_jobbers_api/api_versioning/" rel="noopener noreferrer"&gt;API versioning&lt;/a&gt; is a method that Jobber uses to continuously improve its platform and features while ensuring a predictable process for third-party developers to upgrade their applications. Here's a summary of the approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dangerous and breaking changes are introduced in versioned releases.&lt;/li&gt;
&lt;li&gt;Versions are labeled with date formats (YYYY-MM-DD) and irregularly published to the &lt;a href="https://developer.getjobber.com/docs/changelog" rel="noopener noreferrer"&gt;changelog&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;New versions are added whenever there are breaking or dangerous changes to the API.&lt;/li&gt;
&lt;li&gt;Old versions are supported for 12 months after the release of a newer version.&lt;/li&gt;
&lt;li&gt;If you're using an unsupported version, you'll be automatically upgraded to the earliest supported version, even if it includes breaking changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flj79i1gf4jdr40whj1j4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flj79i1gf4jdr40whj1j4.png" alt="a timeline from January 2022 to June 2023. A bar labeled 2022-03-10 extends from the markers for March 2022 past June 2023. Above it, a bar labeled 2022-01-01 extends from the markers for January 2022 to March 2023. Above the bar a speech bubble points to january 2023 and has the caption "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  API rate limits
&lt;/h3&gt;

&lt;p&gt;Jobber's GraphQL API has &lt;a href="https://developer.getjobber.com/docs/using_jobbers_api/api_rate_limits/" rel="noopener noreferrer"&gt;rate limits&lt;/a&gt; in place to ensure the stability of the platform for all interacting apps. There are two rate limiters in use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DDoS Protection Middleware&lt;/strong&gt;: This limits clients to 2500 requests per 5 minutes on a per app/account basis, rather than by IP address. If an app exceeds this limit, subsequent requests to the same Jobber account will receive a "429 Too Many Requests" error. This limit is generally less restrictive than the GraphQL Query Cost limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GraphQL Query Cost Calculation&lt;/strong&gt;: Each app/account combination has a maximum number of points available for making queries. The cost of each query is deducted from this point pool. Points are restored over time using the &lt;a href="https://en.wikipedia.org/wiki/Leaky_bucket" rel="noopener noreferrer"&gt;leaky bucket algorithm&lt;/a&gt;. If queries are reasonably sized, staying within these limits is straightforward. If you find it challenging, consider introducing delays between queries and caching common results. Error handling can also help regulate queries when throttled.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To &lt;a href="https://developer.getjobber.com/docs/using_jobbers_api/api_rate_limits/#avoiding-rate-limits" rel="noopener noreferrer"&gt;avoid rate limits&lt;/a&gt;, it's recommended to use pagination to collect data in batches, utilize cursor-based pagination for Relay-based queries, and avoid deeply nested queries whenever possible. If nested queries are needed, always apply pagination to connection types. Using these strategies will help you stay within the rate limits and ensure the smooth functioning of your app with Jobber's API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling API Errors
&lt;/h3&gt;

&lt;p&gt;Handling API error scenarios is crucial for maintaining a robust and reliable interaction with Jobber's GraphQL API. Some of the errors you might encounter are &lt;strong&gt;Type Nullability&lt;/strong&gt;, &lt;strong&gt;Scopes &amp;amp; Permissions&lt;/strong&gt;, &lt;strong&gt;App-level Authorization&lt;/strong&gt;, &lt;strong&gt;Jobber Preventative Errors&lt;/strong&gt;, &lt;strong&gt;Scalar Coercion&lt;/strong&gt;, &lt;strong&gt;Rate-Limiting Errors&lt;/strong&gt; and &lt;strong&gt;Inactive User Error&lt;/strong&gt;.&lt;br&gt;
In the &lt;a href="https://dev.to**url**"&gt;Developer Center documentation&lt;/a&gt; you can find an extended explanation of these errors and how to avoid them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the application
&lt;/h2&gt;

&lt;p&gt;During the application building process a Jobber Tester Account was created, linked to your app through the App Authorization flow and GraphQL queries/mutations tested in GraphiQL. Now is the &lt;a href="https://developer.getjobber.com/docs/building_your_app/testing_your_app" rel="noopener noreferrer"&gt;time to test the application&lt;/a&gt; integrated with Jobber and prepare it for publication on the Jobber App Marketplace.&lt;/p&gt;

&lt;p&gt;For your app to be published in Jobber's App Marketplace, there are typically 3 main steps to the testing process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Internal Testing by Developer&lt;/strong&gt;:&lt;br&gt;
Thoroughly test your application internally before submitting it for review. Avoid involving existing Jobber customers in testing without coordinating with your Jobber developer representative and keep information about your app private until it's approved through Jobber's App Review process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;App Review Testing by Jobber&lt;/strong&gt;:&lt;br&gt;
After your internal testing, submit your app for review by Jobber who will verify your app's functionality and may request changes. Your App Marketplace listing details will also be reviewed and Jobber will perform various test cases, including checking proper functionality, data formatting, and data syncing with Jobber.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Beta Testing with Actual Jobber Customers&lt;/strong&gt;:&lt;br&gt;
Once approved by Jobber's App Review team, there's typically a 2-week beta testing period with selected Jobber users. Be prepared to make further changes based on feedback from these users.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By following these steps, you can ensure that your app is thoroughly tested, meets Jobber's requirements, and is ready for publication on the &lt;a href="https://secure.getjobber.com/marketplace" rel="noopener noreferrer"&gt;Jobber App Marketplace&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Load testing the QBO sync app
&lt;/h3&gt;

&lt;p&gt;Load testing is an important component in our application’s release cycle. It ensures that any new feature or application we’re releasing can handle the expected load rates without performance degradation or downtime. For our QBO Sync app, we needed to ensure it could handle anticipated growth in connected users over the next few years, in addition to the expected release volume.&lt;/p&gt;

&lt;p&gt;We split our load testing into two phases: &lt;em&gt;Lightweight&lt;/em&gt; and &lt;em&gt;Robust&lt;/em&gt;. The Lightweight phase aimed to quickly validate our app's ability to handle the load we expected to see, while our Robust phase would implement a tooling based approach that would provide us with more data over time. After investigating several tools we decided on building custom tooling for the lightweight phase.&lt;/p&gt;

&lt;p&gt;Why would you build out custom tooling? We determined that building our own tool would mean writing less code, testing more of our system, and allowing us to gain more confidence in our approach.&lt;/p&gt;

&lt;p&gt;We built a system that would trigger events inside of the main Jobber Online platform, which would then send webhooks to our app. By triggering events on test accounts inside of our main platform rather than mocking them, we were able to test a more complete picture of the application. This also meant that we didn't have to mock out our &lt;a href="https://developer.getjobber.com/docs/using_jobbers_api/setting_up_webhooks/#verify-authenticity-of-the-webhook" rel="noopener noreferrer"&gt;webhook authenticity check&lt;/a&gt; which had posed a challenge in our investigation.&lt;/p&gt;

&lt;p&gt;Having a purpose-built solution quickly available made the most sense for the needs of this project and helped us bridge the gap of evaluating and implementing a robust load testing tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  End-to-End testing the QBO sync app
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhiq721bg8zdwd13in51p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhiq721bg8zdwd13in51p.png" alt="A flow chart describing the end-to-end testing procedure for the QBO app. First, A seeding script script creates data in the Jobber platform which sends webhooks to the QBO sync app. This then responds by syncing that data into Quickbooks Online. Second, a testing script verifies that the expected data is present in quickbooks online. Lastly, a teardown script removes the seed data from Jobber. "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The nature of our application being a passthrough for data between Jobber and Quickboooks Online meant we needed to come up with a novel solution for end-to-end testing. In the early stages of the project, our end to end testing was done manually by engineers following lengthy Test Case documents. One of the main goals of our end-to-end testing was to replace this manual testing with an automated, repeatable system. This would be both easier and faster to perform than manual testing, meaning that we could perform the tests more often and eventually find bugs earlier. &lt;/p&gt;

&lt;p&gt;Conventionally, end-to-end tests would be contained inside a single application. The trigger for a test would be some interaction in the UI, and the expected result would be a change in the interface of the same application. &lt;/p&gt;

&lt;p&gt;The trigger for most actions in our application are webhooks sent by Jobber, which are processed by our application, creating or editing data inside QuickBooks Online. This presented a challenge: both the triggers for our test events, and the results we needed to verify were outside of our application, not to mention being in separate systems themselves. We realized quickly that a conventional approach to end-to-end testing wouldn’t work for us, and we’d have to build our own system. &lt;/p&gt;

&lt;p&gt;To test our app’s workflow, we separated our testing framework into three components. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data Seeding&lt;/strong&gt;: We built a &lt;a href="https://www.rubyguides.com/2019/02/ruby-rake/" rel="noopener noreferrer"&gt;rake task&lt;/a&gt; that creates and edits data inside of Jobber, which in turn sends webhooks to our application. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verification&lt;/strong&gt;: Using &lt;a href="https://www.rubyguides.com/2018/07/rspec-tutorial/" rel="noopener noreferrer"&gt;Rspec&lt;/a&gt; as a test framework, we make requests to the Quickbooks API to verify the data that was created in the first step is in the correct state in QBO. These tests are triggered by a rake task and are run separately from our unit tests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cleanup&lt;/strong&gt;: The QBO API doesn't allow for deletion of all of the models (i.e. inventory items) that we are testing, instead allowing developers to reset the entirety of the data in a sandbox. We then build another small rake task to wipe the test data created by the data seeding from Jobber. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Launching the application
&lt;/h2&gt;

&lt;p&gt;After completing development and testing, it is time to publish the application so that Service Providers can start using it. The publishing process involves providing the listing details to appear in Jobber’s App Marketplace, having the Jobber team review the application, and setting up an entry point for users if your application has a settings page or if you want them to manage functionalities within your application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Providing listing details
&lt;/h3&gt;

&lt;p&gt;Upon publication, the listing on the marketplace will be characterized by the utilization of the App name, Developer name, App description, Manage App URL (optional for a basic listing), Features or benefits, Gallery images, and App logo fields. The finalized appearance of the app will look like the following:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffsh6l6dgk7uk9c8uwava.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffsh6l6dgk7uk9c8uwava.png" alt="A listing for an example app in the Jobber app marketplace. We can see the app logo, app name, description, features and benefits, and a button that the user click to connect the app to their Jobber account"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Review process
&lt;/h3&gt;

&lt;p&gt;When submitting your application, Jobber will prompt you to fulfill the &lt;a href="https://forms.gle/etnZAQN8F6X62GyMA" rel="noopener noreferrer"&gt;App Pre-Submission Checklist&lt;/a&gt;. For additional details on the app testing procedure, refer to the &lt;a href="https://forms.gle/etnZAQN8F6X62GyMA" rel="noopener noreferrer"&gt;Testing An App&lt;/a&gt; section. To initiate a review of your app, simply click on the three dots action menu adjacent to your app within the Manage Apps view. If you have no plans to publish your app on Jobber's App Marketplace, please consult the Custom Integrations section.&lt;/p&gt;

&lt;p&gt;During the review process, Jobber may request any changes necessary to best integrate with Jobber's ecosystem. Once the review is complete, the app will be ready for users. The review process can also be canceled at any time if desired.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding a Manage App button
&lt;/h3&gt;

&lt;p&gt;If the application includes a settings page or another page to manage functionalities, you can leverage the Manage App URL field in our Developer Center. This allows you to unveil a new button for Jobber admin users directly from the App Marketplace listing. The button will be visible exclusively to Jobber admin users who are exploring your App Marketplace listing post their Jobber account is successfully linked to your application. More information on the &lt;a href="https://developer.getjobber.com/docs/publishing_your_app/manage_app_button/" rel="noopener noreferrer"&gt;Developer Center&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Building and launching an application using Jobber’s platform is straightforward and the Developer Center provides you with all the information you need.&lt;/p&gt;

&lt;p&gt;In the third and final post, we will discuss the challenges we encountered, share some best practices for building an application on Jobber's platform, delve into the lessons learned, highlight significant successes, and outline our future plans.&lt;/p&gt;




&lt;h2&gt;
  
  
  About Jobber
&lt;/h2&gt;

&lt;p&gt;Our awesome Jobber technology teams span across Payments, Infrastructure, AI/ML, Business Workflows &amp;amp; Communications. We work on cutting edge &amp;amp; modern tech stacks using React, React Native, Ruby on Rails, &amp;amp; GraphQL.&lt;/p&gt;

&lt;p&gt;If you want to be a part of a collaborative work culture, help small home service businesses scale and create a positive impact on our communities, then visit our &lt;a href="https://getjobber.com/about/careers?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=eng_blog" rel="noopener noreferrer"&gt;careers&lt;/a&gt; site to learn more!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;A post by Christian do Prado Silva, Diogo Gallo, and Christian Ellinger&lt;/em&gt;&lt;/p&gt;

</description>
      <category>integration</category>
      <category>graphql</category>
      <category>testing</category>
    </item>
    <item>
      <title>Building an App in Jobber Platform</title>
      <dc:creator>Diogo Gallo</dc:creator>
      <pubDate>Wed, 27 Sep 2023 16:49:14 +0000</pubDate>
      <link>https://forem.com/jobber/building-an-app-in-jobber-platform-57aj</link>
      <guid>https://forem.com/jobber/building-an-app-in-jobber-platform-57aj</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the first post of a three-part series&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;At Jobber we strive to help thousands of Home Service Pros worldwide in managing their business, and part of the mission is to rethink how they can do that more efficiently. In service of this mission, we built the &lt;a href="https://developer.getjobber.com/landing" rel="noopener noreferrer"&gt;Jobber’s Developer Center&lt;/a&gt; to empower developers to integrate and create applications to do just that.&lt;/p&gt;

&lt;p&gt;The Jobber’s Developer Center offers a modern technology stack that facilitates the creation of applications. Companies such as &lt;a href="https://getjobber.com/integrations/companycam/" rel="noopener noreferrer"&gt;CompanyCam&lt;/a&gt; and &lt;a href="https://getjobber.com/integrations/thumbtack/" rel="noopener noreferrer"&gt;Thumbtack&lt;/a&gt; have already developed applications that enhance the Service Provider experience with Jobber. Our most recent integration was with Intuit QuickBooks Online. An internal team at Jobber was tasked with envisioning and building a seamless data synchronization between Jobber and QuickBooks Online.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frdlwl6z6thx7fcwbvn51.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frdlwl6z6thx7fcwbvn51.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Although the synchronization between the systems existed in Jobber since its inception, it was not user-friendly, causing friction for Service Providers who required cohesive data in both systems. Built entirely on the Jobber’s platform, using the &lt;a href="https://developer.getjobber.com/docs/#about-jobber" rel="noopener noreferrer"&gt;GraphQL API&lt;/a&gt; and the &lt;a href="https://atlantis.getjobber.com/" rel="noopener noreferrer"&gt;Jobber Design System&lt;/a&gt;, the new solution expanded the capabilities of the platform and established a foundation for future applications to be built upon.&lt;/p&gt;

&lt;p&gt;In this three-part series, we will dive into the pain points of the previous QuickBooks Online (QBO) integration, the principles we followed to develop the new application, the main architectural decisions, as well as provide details and best practices to help developers successfully build and launch their applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rebuilding the QBO Integration
&lt;/h2&gt;

&lt;p&gt;Nearly every small business begins by using an accounting solution before incorporating a workflow solution like Jobber. Effective accounting and finance practices are essential for running a small business successfully. We want our service providers (SPs) to be able to manage their businesses in Jobber while seamlessly integrating with QuickBooks Online, knowing that their accountants won't be pulling hairs at the end of each month.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl8tvb2gj88ypl02unwac.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl8tvb2gj88ypl02unwac.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, the previous integration between Jobber and QBO had some drawbacks that hindered users' overall experience. The initial synchronization process was challenging and posed risks, often requiring extensive support from the Jobber Success team. It was difficult to determine which platform, Jobber or QBO, was the source of certain issues. The manual synchronization process did not meet consumer-grade expectations, such as simplicity and intuitiveness, and allowed unclear errors to accumulate. Moreover, maintaining and enhancing the integration had become increasingly difficult, resulting in long wait times for new features and bug fixes.&lt;/p&gt;

&lt;p&gt;Early on in the project, the team decided to rebuild the integration from scratch, aiming to make it simpler, more capable, and fully self-serve. Throughout this endeavor, we established guiding principles to aid our decision-making process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Focus on simplicity, predictability, and reliability:&lt;/strong&gt; We can easily explain to SPs what will happen or has happened when they use the integration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Establish Jobber as the source of truth:&lt;/strong&gt; Optimize the integration for SPs who rely on Jobber as their primary source of information for quoting, scheduling, invoicing, and receiving payments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Develop as a first-party application:&lt;/strong&gt; Function as a first-party service independent of Jobber, leveraging our platform infrastructure and expanding its capabilities to support similar integrations, enabling others to build comparable solutions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automated and near real-time synchronization:&lt;/strong&gt; Eliminate the need for manual syncing; every change made in Jobber will be instantly captured and pushed to QBO.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core of the integration is an ongoing, automatic sync of data from Jobber to QBO. This process is supported by a simple, self-serve user interface that simplifies configuration settings and error handling. &lt;/p&gt;

&lt;p&gt;Below we have some highlights of this solution. For more detailed information on how the integration works, please refer to our &lt;a href="https://help.getjobber.com/hc/en-us/articles/10487017203223-How-Items-Sync-Between-Jobber-and-QuickBooks-Online-NEW-QuickBooks-Integration" rel="noopener noreferrer"&gt;Jobber Help Center&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once you connect the app and have your ongoing sync enabled, every change in Jobber will be sent to QBO and you can keep track in the Sync Activity dashboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feyjtftsco604zzjhl4v4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feyjtftsco604zzjhl4v4.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A detailed view of an item that has been successfully synced shows the date and time it has been synced and also links to the respective item in Jobber and QBO.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0bk7m73qpmw71ynaoztm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0bk7m73qpmw71ynaoztm.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once an item is synced with a warning or error, the detailed view shows a clear message and provides you with actionable options to fix the problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0tptki8now6l9vspgplg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0tptki8now6l9vspgplg.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In Jobber, if there is any sync activity that requires your attention you'll see an alert in your top navigation. Click the alert to view the sync activity dashboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcgsjljrbwbh6ckodd7cy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcgsjljrbwbh6ckodd7cy.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;During the import, if client duplicates are detected you will have the option of selecting the client in Jobber that the client in QuickBooks will be mapped to. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwrag738pwzqu6z6q2m2y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwrag738pwzqu6z6q2m2y.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if you want to change any default settings, the intuitive Settings page is where you can customize when each item syncs with Jobber.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs9t4bjzi1dacadv99grh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs9t4bjzi1dacadv99grh.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Architectural decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  First-party application
&lt;/h3&gt;

&lt;p&gt;One of the initial architectural decisions the team had to make was how to rebuild this integration. Several tentative plans were considered for rebuilding the integration within our monolith. However, there were always constraints when it came to maintaining the existing integration while simultaneously incorporating the new one into the same codebase.&lt;/p&gt;

&lt;p&gt;Developing an application outside of our monolith would provide the project with a greenfield opportunity to address all existing issues with minimal worry about backward compatibility. Additionally, it would allow us to take advantage of the nascent Jobber platform, expanding its capabilities to support similar accounting integrations or any other features that would benefit from the platform improvements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tech Stack
&lt;/h3&gt;

&lt;p&gt;After deciding that it would be an external application, we had the option to select a technology stack that was better suited to its needs. Certain characteristics were prioritized in making this decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt; it should be able to perform an object sync instantaneously, i.e. in milliseconds&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scalability:&lt;/strong&gt; it should support our current users already integrating with QBO and also support Jobber’s growth&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Operability:&lt;/strong&gt; it should be easy and efficient to operate, easy to maintain and troubleshoot, with high availability and reliability due to near real-time live sync.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Maintainability:&lt;/strong&gt; it should be easy for the development team to include new features and resolve any issues without the need for major refactors in the future.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With those characteristics in mind, we chose Ruby and Rails for the backend, powered by a GraphQL API, React library for the front end, and Sidekiq for background job processing responsible for all the syncs. This tech stack mimics the core of our Jobber application, which allowed us to quickly begin development, and minimized the learning curve for the development team. We also took the opportunity to use the most recent version of Ruby and Rails at the time (3.1 and 7 respectively), opening opportunities to explore new features such as Native Encryption and incorporating a type checking &lt;a href="https://sorbet.org/" rel="noopener noreferrer"&gt;Sorbet&lt;/a&gt;, built by Stripe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Change Capture
&lt;/h3&gt;

&lt;p&gt;To establish an automatic and real-time sync between Jobber and QBO, it was necessary to capture any changes made to objects in the Jobber application and initiate the synchronization process.&lt;/p&gt;

&lt;p&gt;Within Jobber, we already have a mechanism for capturing data changes and notifying subscribers who can react to those changes. One of these subscribers is powered by our platform, which sends &lt;a href="https://developer.getjobber.com/docs/build_with_jobber/webhooks" rel="noopener noreferrer"&gt;webhooks&lt;/a&gt; to external applications that have registered for events.&lt;/p&gt;

&lt;p&gt;As an external application, it was crucial to not miss any of these webhooks; otherwise, the synchronization process would not be triggered, and both systems would be out of sync. To ensure the processing of all webhook requests, an &lt;a href="https://aws.amazon.com/lambda/" rel="noopener noreferrer"&gt;AWS Lambda&lt;/a&gt; function was configured with a &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html" rel="noopener noreferrer"&gt;function URL&lt;/a&gt;— an HTTPS endpoint used for subscribing to webhook events. Upon receiving an event, the Lambda function &lt;a href="https://developer.getjobber.com/docs/using_jobbers_api/setting_up_webhooks/#verify-authenticity-of-the-webhook" rel="noopener noreferrer"&gt;validates the authenticity of the webhook request&lt;/a&gt;, ensuring it originated from Jobber, and then sends the event to an AWS SQS queue, which is eventually consumed by the QBO App.&lt;/p&gt;

&lt;p&gt;Using AWS Lambda ensures high availability for receiving webhook requests, and utilizing AWS SQS allows for the necessary throughput to process all the updates without creating a bottleneck in the application, especially during periods of high activity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8lr5m4txbnddcqlecqbx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8lr5m4txbnddcqlecqbx.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Localstack to mimic AWS environment
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3yi9ph3culrr2fyxqvz8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3yi9ph3culrr2fyxqvz8.png" alt="Image description"&gt;&lt;/a&gt;&lt;br&gt;
We decided early on that we wanted our local development environments to match the production infrastructure as closely as possible. To do this, we used LocalStack to emulate the cloud services (AWS Lambda and AWS SQS) that our application required. This allowed us to build and test the code which interacted with these services without having to provision AWS resources for each developer, saving us both time and money.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sorbet usage
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdykepb3vj9iyq2p2x03e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdykepb3vj9iyq2p2x03e.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sorbet is a type checking gem built by Stripe, which adds static typing to Ruby. We decided to adopt it for this project to improve code readability, but also as a proof of concept before potentially adopting it into the larger Jobber codebase. In practice, we found both benefits and drawbacks to using Sorbet, as is often the case with any tool. The improved code readability and method introspection were noticeably beneficial during development, however, the new system made bringing additional developers onto the project difficult as they were unfamiliar with the new type system. &lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;As we can see in this first post of the three-part series, there are several factors and decisions involved while building an integration application. Jobber created the Developer Center with that in mind, making it straightforward for any developer to develop their applications and integrate with Jobber. In the second post we will get into the technical details of how you also could build an application and everything you need to do from the app creation to the launch.&lt;/p&gt;

&lt;h3&gt;
  
  
  About Jobber
&lt;/h3&gt;

&lt;p&gt;Our awesome Jobber technology teams span across Payments, Infrastructure, AI/ML, Business Workflows &amp;amp; Communications. We work on cutting edge &amp;amp; modern tech stacks using React, React Native, Ruby on Rails, &amp;amp; GraphQL. &lt;/p&gt;

&lt;p&gt;If you want to be a part of a collaborative work culture, help small home service businesses scale and create a positive impact on our communities, then visit our &lt;a href="https://getjobber.com/about/careers?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=eng_blog" rel="noopener noreferrer"&gt;careers&lt;/a&gt; site to learn more!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;A post by Christian Ellinger, Christian do Prado Silva and Diogo Gallo&lt;/em&gt;&lt;/p&gt;

</description>
      <category>integration</category>
      <category>architecture</category>
      <category>graphql</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Goals Hurt</title>
      <dc:creator>John Zittlau</dc:creator>
      <pubDate>Wed, 09 Aug 2023 17:05:32 +0000</pubDate>
      <link>https://forem.com/jobber/goals-hurt-13nl</link>
      <guid>https://forem.com/jobber/goals-hurt-13nl</guid>
      <description>&lt;p&gt;I was recently listening to a podcast where the speaker was someone being introduced as very successful. By many metrics they were. They had lots of money, C-level at a respected company, had competed in world cups and been nationally ranked in a number of diverse sports. Yes, they had accomplished lots. As they were sharing their thoughts on how to succeed, one of their primary thesis was to set big audacious personal goals. Don't set a goal of being worth 1 Million, set a goal of personally being worth 100 Million (or maybe even a billion). The reason for a very hard goal (they said) is that if you set an easier personal goal, then when you achieve it you'll be lost. Cast adrift. Have nothing to work for. You'll have to spend time finding a new goal and you may find that you've "wasted" your time on your old goal as it doesn't lead directly to the new one.&lt;/p&gt;

&lt;p&gt;I could not relate to this way of thinking. I have no problem finding valuable things to do without audacious goals and by many measures I view myself as successful. Further, I do not believe that formally setting goals has contributed to my success. In fact, as you read further, you'll see that I believe formal goal setting to be more of a hindrance than a benefit for me.&lt;/p&gt;

&lt;p&gt;We're constantly hearing from "successful" people that they owe their success in large part to setting goals. That the goals gave them the drive to persevere. To push through obstacles and make those goals a reality. I'm sure they believe that. I'm not sure it is a universal constant. Or that it is the one-true-way to success.&lt;/p&gt;

&lt;p&gt;The belief that setting personal goals leads to success is so pervasive that it has taken on the status of a law of nature. Set big goals. Drive to achieve them. Profit. Many companies see personal goal setting as a critical part of career development and the road to producing high-performing individuals. Everyone is tasked with creating goals for themselves and then measured on how well they achieve those goals.&lt;/p&gt;

&lt;p&gt;This is such a universal belief that it is only now, after almost three decades of professional life, that I've finally developed the confidence and courage to say that goals don't work for me. For most of my career, I've figured something must be wrong with me. Everyone says set goals and win. I set goals and could care less about them. The presence of a goal does almost nothing to push me along. And if I have been forced to set a big goal and don't achieve it, I feel demotivated. But not demotivated in a way that makes me want to try and achieve my next goal. Demotivated in a way that makes me want to simply give up.&lt;/p&gt;

&lt;p&gt;Let's be clear on one thing here. I'm talking about personal goals. And also mostly the bigger goals (anything with a timespan larger than a month). I do set small goals, but not as a "forcing function", but rather as a prioritization tool. I also see team goals differently. Setting reasonable goals for a team is valuable. It helps ensure the team is moving in the same direction. It likely is helping team's prioritize, much as small personal goals help me.&lt;/p&gt;

&lt;p&gt;As I've said, I do set little goals for the day or weekend or maybe week (a month on the extreme outside). That does help me prioritize what to do, but it doesn't drive me to do anything. And if I don't achieve my daily goal, I'm OK with that. As long as I'm happy with how I spent my time, then I'm good. Maybe that is the one goal that works for me. Be sure I'm happy with where I put my energies for that period of time. At work, this means I'm happy when I'm confident that what I did helped Jobber and was among the most important things I could be doing at that time.&lt;/p&gt;

&lt;p&gt;On the weekend, being happy with how I spent my time could mean that I'm happy I spent a bunch of hours in front of the TV or video game. Or maybe I'm happy having done some woodworking project. Or I created an awesome Sunday meal. Or maybe I did spend the weekend diving into some new learning that will help my career. Clearly, I'm not a driven person. The journey is at least as important as the destination.&lt;/p&gt;

&lt;p&gt;I remember the first time I worked somewhere that asked me to set career goals. I'd never done it before and was happy with where I was in my career, but set the goal anyway. Then a year later I hadn't achieved the goals I'd set. Boy did I not like that. But as I said above, it didn't motivate me to change anything. It just made me question if I wanted to stay at the company that was forcing me to set goals. It was already clear to me then that setting goals was not helping me achieve in any way and in fact was just making me less happy with life.&lt;/p&gt;

&lt;p&gt;I suspect we hear about this path to success a disproportionate amount of time. It seems far less motivational to hear from those who "just fell into something". That doesn't provide for any life hacks to take. Setting goals is something anyone can do. Getting lucky isn't. So we hear from those who have a "solution" we all can do. The simple advice to riches draws attention. It promises a quick fix. Who doesn't want a quick fix that will put them on magazine covers.&lt;/p&gt;

&lt;p&gt;I believe now that in many cases those who credit setting goals as the driver to their success succeeded not because they set goals, but that they have a "goal driven" personality. They are ready to put everything else aside to "succeed". Setting goals isn't changing their approach to life. Their approach to life includes setting goals.&lt;/p&gt;

&lt;p&gt;There are others, like me, who are happy to let life take us where it will. Where setting goals is contradictory to what wakes us up in the morning. Where defining a goal is hard and caring about that goal is even harder. For us, when the company says "create goals" we grumble and do it because we have to, but we don't like it. And it doesn't help us to succeed. If anything, when we review the goals, we're irritated and stressed that they are there and want to find a safe place where we can grow without having decided how we will grow in advance.&lt;/p&gt;

&lt;p&gt;I'm now a Distinguished Software Engineer. I think I can point to that as evidence that I've "succeeded" as a developer. Can I credit goal setting for getting here? Absolutely not. In fact I had to fight through hating the goal setting process to continue on as a developer. The temptation to find someplace safe without goals has always been strong.&lt;/p&gt;

&lt;p&gt;You may have noticed that I put "succeed" in quotes everywhere. That is because in too many cases when I see someone has "succeeded", it is in one facet of their life at the expense of some other. Even I did that above where I tied my title to being a signifier of success. Often money/career is viewed as success. Family/friends don't even enter the conversation.&lt;/p&gt;

&lt;p&gt;I confidently say I've succeeded without the quotes. I may not have Elon Musk levels of money, but I get by. I may not have huge followers on some social network, but I have solid friends. And I have a solid family. Everything in balance. I'm happy. At least until the next time I'm asked to set a personal goal.&lt;/p&gt;

&lt;p&gt;So who's with me? Are goals essential for success? Do they help or hinder you?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About Jobber&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our awesome Jobber technology teams span across Payments, Infrastructure, AI/ML, Business Workflows &amp;amp; Communications. We work on cutting edge &amp;amp; modern tech stacks using React, React Native, Ruby on Rails, &amp;amp; GraphQL.&lt;/p&gt;

&lt;p&gt;If you want to be a part of a collaborative work culture, help small home service businesses scale and create a positive impact on our communities, then visit our &lt;a href="https://getjobber.com/about/careers?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=eng_blog"&gt;careers&lt;/a&gt; site to learn more!&lt;/p&gt;

</description>
      <category>mentoring</category>
      <category>career</category>
      <category>management</category>
    </item>
    <item>
      <title>Jobber's React Native migration - Success!</title>
      <dc:creator>Ryan Jones</dc:creator>
      <pubDate>Wed, 21 Jun 2023 17:44:34 +0000</pubDate>
      <link>https://forem.com/jobber/jobbers-react-native-migration-success-600</link>
      <guid>https://forem.com/jobber/jobbers-react-native-migration-success-600</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsjwm80aq0ytt6ry8hnnr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsjwm80aq0ytt6ry8hnnr.png" alt="Jobber"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;January ‘23 was an exciting month. Not only did we raise a &lt;a href="https://betakit.com/jobber-closes-100-million-usd-series-d-amid-strong-demand-for-home-services/" rel="noopener noreferrer"&gt;Series D&lt;/a&gt; but we launched our new version of the Jobber app which is now powered by React Native. This post is mainly coming from the engineering lens. If that’s your jam, continue reading!&lt;/p&gt;

&lt;p&gt;I’m going to try and provide as much insight as I can to what happens ‘behind the curtains’ on one of these types of projects.&lt;/p&gt;

&lt;p&gt;Don’t care about the story? Jump to the results.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision
&lt;/h2&gt;

&lt;p&gt;Jobber has been growing fast. With that growth has come with it a set of expectations on both of our online and mobile offerings. Our mobile offering in particular needed some TLC (Tender Loving Care). We’d been running on the ember.js framework (wrapped webview) which we had launched back in Aug 2015.&lt;/p&gt;

&lt;p&gt;The reality is that ember.js wasn’t built for mobile apps and it was starting to show. Basic native functionality that customers expect (pull down to refresh, swipe for nav) either took a long time to build or just wasn’t giving us that ‘native mobile app’ feel. As the mobile app volume started to increase we started to hit odd edge cases/errors and it was becoming really tough to debug the issues. In addition to all of that it was starting to get really hard to have multiple teams working in the app’s codebase.&lt;/p&gt;

&lt;p&gt;The state of the app was normal for a startup that was trying to find product-market fit but we were past that stage and our customers deserved better. In October ‘21 we decided to do an ‘all stop’ and focus 80% of our effort to accelerate the rebuild of our mobile app.&lt;/p&gt;

&lt;h2&gt;
  
  
  History
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7jcm95bta8tbu5znx9qi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7jcm95bta8tbu5znx9qi.png" alt="Jobber React"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Alright, I know what you’re thinking. ‘A rebuild!? Are you nuts?’. I agree with you. I absolutely, to the core of my being, hate re-writes. I’ve seen more than enough go sideways in my career. Everything from under estimating to scope increases to leadership losing their appetite for a hold on feature work.&lt;/p&gt;

&lt;p&gt;But, let’s rewind a bit. Context matters. We didn’t go into this one blind. We did a lot of exploration before we pulled the pin, here’s what we did before we made the decision to accelerate the build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2020 - March&lt;/strong&gt; - Decision to move forward with React Native as our mobile framework&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2020 - April&lt;/strong&gt; - Explore how we can migrate to React Native without a big bang. Pulled React Native into the app ‘behind the scenes’&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2020 - August&lt;/strong&gt; - Decision to adopt GraphQL as our API layer (to power mobile, web and our public API)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2020 - September&lt;/strong&gt; - First 2 features launched in React Native&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2021 - January&lt;/strong&gt; - A few more features launched in React Native&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2021 - March&lt;/strong&gt; - Replaced the Navigation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2021 - July&lt;/strong&gt; - A few more features rebuilt in React Native&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2021 - October&lt;/strong&gt; - Decision to Accelerate build&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We had done a lot of way finding along the way and even started to migrate the app without having to do a ‘big bang’. This wasn’t a full YOLO moment, but we definitely had some hurdles to overcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scope
&lt;/h2&gt;

&lt;p&gt;Alright, so we’re going to accelerate the build. What does this mean? Well, it means a lot of other things are going to need to be accelerated. Here’s the list of known hurdles that we had in front of us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Train 50+ software engineers in React Native&lt;/li&gt;
&lt;li&gt;Train 50+ software engineers in GraphQL&lt;/li&gt;
&lt;li&gt;We need to design the new experience (we opted to improve a few key experiences)&lt;/li&gt;
&lt;li&gt;Build out our mobile design system&lt;/li&gt;
&lt;li&gt;Hire 30 more software engineers in the middle of the build and ramp them up 😱&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I remember laying in bed the night after we (myself, CTO and VP, Product)  made the call to accelerate the build, staring at the ceiling with a mix of excitement and dread. On one hand, this would be a huge win for the mobile app, on the other hand we had massive hurdles to overcome. Any one of the hurdles by itself would be tough to overcome. &lt;/p&gt;

&lt;h2&gt;
  
  
  The goals
&lt;/h2&gt;

&lt;p&gt;These types of projects are tough because you’re trying to accomplish multiple things at once and you end up with competing priorities. We landed on these prioritized goals for the project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The app is easier and more productive to develop consumer-grade experiences in&lt;/li&gt;
&lt;li&gt;Our app is more reliable – it has fewer bugs, fewer crashes, fewer white screens (unhandled exceptions)&lt;/li&gt;
&lt;li&gt;We have improved the user experience for our SPs&lt;/li&gt;
&lt;li&gt;Our calendar is more powerful&lt;/li&gt;
&lt;li&gt;Our app meets a minimum bar for performance&lt;/li&gt;
&lt;li&gt;All of the above is live by Aug 30/2022&lt;/li&gt;
&lt;li&gt;There is no ember.js left in the app by Feb 15/2023&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We landed on Aug 30 based on a few high level estimates from a few teams who had the most experience with React Native and GraphQL. Since we had made the decision to accelerate, it was better to start executing and refine those dates as we progressed.&lt;/p&gt;

&lt;p&gt;Every week we would meet to review each of the above goals and any associated metrics as a 🔴/🟡/🟢 projection. Some of them are tough to measure (e.g. How do you measure engineering productivity? Lack of observability in the old app ruled out many direct comparisons on performance and stability).&lt;/p&gt;

&lt;h2&gt;
  
  
  The build
&lt;/h2&gt;

&lt;p&gt;We’re a big fan of the triad model at Jobber where we have equal discipline (eng/product/design) representation at the table as we build. We had a triad overseeing the whole project. Within the project we had multiple teams shipping within their areas (e.g. Our fintech teams would re-write the fintech functionality). &lt;/p&gt;

&lt;p&gt;I absolutely hate ‘like for like’ conversions so we opted to add some improved functionality in this project. Specifically a few areas that we couldn’t have made better in the old ember.js framework. They were an improved schedule/calendar, revamped settings drawer, revamped file upload control and much better form UX. We accepted the scope increase for the benefit of our customers.&lt;/p&gt;

&lt;p&gt;From November 2022 to Feb 2023 you can imagine a flurry of activity of designing, building, collaborating and shipping to the app. Here’s the noteworthy milestones.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;April 2022&lt;/strong&gt; - Alpha ships &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;July 2022&lt;/strong&gt; - Beta ships&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;August 2022&lt;/strong&gt; - Code freeze for GA (‘Opt in to new version’ for users)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sept 2022&lt;/strong&gt; - Rolled out new functionality to new users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oct 2022&lt;/strong&gt; - First wave of forced rollout&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nov 2022&lt;/strong&gt; - Second wave of forced rollout&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nov 2022&lt;/strong&gt; - Oh 💩- Low perf device issues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dec 2022&lt;/strong&gt; - Third wave of forced rollout&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jan 2023&lt;/strong&gt; - Final wave of forced rollout to low perf devices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feb 2023&lt;/strong&gt; - Ember.js fully removed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ultimately we managed to ship the new React Native functionality by August but it took a few more months to fully roll it out and remove ember.js. This was great as we had the better version of the app in our customers hands as soon as possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Things that went well
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Roll out React Native beside ember.js
&lt;/h4&gt;

&lt;p&gt;We rolled out the base framework underneath ember.js to help prove out the build and migrated some core components such as the navigation bar, home screen and communication center. &lt;/p&gt;

&lt;p&gt;This gave us enough confidence in the tech to push forward and make the full conversion.&lt;/p&gt;

&lt;h4&gt;
  
  
  Tech lead syncs
&lt;/h4&gt;

&lt;p&gt;Every week the tech leads from the teams would sync up and either highlight upcoming changes or raise a hand to help get support from other teams.&lt;/p&gt;

&lt;h4&gt;
  
  
  Strike teams on common components
&lt;/h4&gt;

&lt;p&gt;As common components were identified, having an individual contributor from each of the teams work on the shared components as a short-term strike team worked well.&lt;/p&gt;

&lt;h4&gt;
  
  
  Mobile components team
&lt;/h4&gt;

&lt;p&gt;We spun up a new team to help support the mobile iOS/Android work and added to our components team which helped accelerate all of the other teams. This approach was so valuable that we’ve mirrored a similar approach as we migrate our online interface to React.js&lt;/p&gt;

&lt;h4&gt;
  
  
  User testing
&lt;/h4&gt;

&lt;p&gt;Testing functionality with users and non-users gave us early feedback that allowed us to iterate before we invested in the build. &lt;/p&gt;

&lt;p&gt;Through the project, our entire processes around design review and user testing leveled up. We continue to benefit from our newly established best practices around early user engagement feedback.&lt;/p&gt;

&lt;h4&gt;
  
  
  Observability
&lt;/h4&gt;

&lt;p&gt;Purchased and implemented &lt;a href="https://sentry.io/welcome/" rel="noopener noreferrer"&gt;sentry&lt;/a&gt; to provide crash/performance metrics and make finding, assigning and fixing issues easier for teams. &lt;/p&gt;

&lt;h4&gt;
  
  
  Team health
&lt;/h4&gt;

&lt;p&gt;Engage teams with qualitative surveys to understand if our code health, build speed and release process was improving.&lt;/p&gt;

&lt;h4&gt;
  
  
  Migration metrics
&lt;/h4&gt;

&lt;p&gt;We kept a keen eye on our technical metrics to make sure that we were progressing to plan. A few of these were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ember.js vs. React Native routes converted&lt;/li&gt;
&lt;li&gt;GraphQL adoption metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Bitrise
&lt;/h4&gt;

&lt;p&gt;Unified both iOS and Android building to &lt;a href="https://bitrise.io/" rel="noopener noreferrer"&gt;bitrise&lt;/a&gt; for our mobile build pipeline. Much better than the older Buddy Build system which was purchased by Apple, put into hibernation, and then shut down by Apple.&lt;/p&gt;

&lt;h4&gt;
  
  
  Runway
&lt;/h4&gt;

&lt;p&gt;Implemented &lt;a href="https://www.runway.team/" rel="noopener noreferrer"&gt;runway&lt;/a&gt; to help with our mobile release process.&lt;/p&gt;

&lt;h4&gt;
  
  
  React native upgrade
&lt;/h4&gt;

&lt;p&gt;We started to hit a few snags along the way due to being behind a few releases of React Native. Popping off the upgrade helped us get over those hurdles.&lt;/p&gt;

&lt;h4&gt;
  
  
  Split
&lt;/h4&gt;

&lt;p&gt;We implemented &lt;a href="https://www.split.io/" rel="noopener noreferrer"&gt;split&lt;/a&gt; on mobile which helped us control the rollout and alpha/betas with feature flags.&lt;/p&gt;

&lt;h3&gt;
  
  
  Things that didn’t go well
&lt;/h3&gt;

&lt;p&gt;Every project of this size will have bumps and bruises along the way. &lt;/p&gt;

&lt;h4&gt;
  
  
  Fine grained iterative deploys
&lt;/h4&gt;

&lt;p&gt;We originally wanted to deploy each screen behind feature flags to allow the most granular level of deploys, but when you mixed in different SaaS plan types it became really hard to manage. We also ran into complex navigation issues between ember.js nav stack and React Native’s nav stack. We did use feature flags but we ended up leaving them at the SaaS plan level.&lt;/p&gt;

&lt;p&gt;We took 3 runs at this before bailing. We just couldn’t manage the complexity of multiple frameworks effectively.&lt;/p&gt;

&lt;h4&gt;
  
  
  1 dedicated principal IC
&lt;/h4&gt;

&lt;p&gt;We should have assigned 1 principal engineer to the project to help coordinate and overcome some of the engineering challenges. We had 3 key individual contributors take on this mantle 1/3rd of the way through the project, but it would have been better to assign this early on.&lt;/p&gt;

&lt;h4&gt;
  
  
  Oh 💩- Low performance device issues
&lt;/h4&gt;

&lt;p&gt;Woof. We took a few app review hits on this one. We started to see a few bad reviews come in regarding performance and we realized that we didn’t have a wide enough set of test devices on Android. We didn’t see this in our alpha/beta group as we learned that they didn’t reflect the device spread for the general release. The other issue was that our simulators didn’t reflect the realities of the slower devices like we had assumed. &lt;/p&gt;

&lt;p&gt;We rectified this quickly by: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Implementing a bunch of optimizations via memoization/use callback (using Shopify’s lovely &lt;a href="https://github.com/Shopify/react-native-performance" rel="noopener noreferrer"&gt;perf libs&lt;/a&gt; ❤️🇨🇦) to improve performance.&lt;/li&gt;
&lt;li&gt;Purchasing and distributing the lowest performing Android phones (the slowest android phones on the market) to every team to be used for all testing going forward.&lt;/li&gt;
&lt;li&gt;Build out a test framework on AWS’s Mobile device farm w/ Appium to smoke test across many devices.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Datadog migration
&lt;/h4&gt;

&lt;p&gt;We needed to migrate from New Relic to Datadog through Oct/Dec due to New Relic not willing to budge on user licenses. This was expected but really hampered a few of our teams during the project.&lt;/p&gt;

&lt;h4&gt;
  
  
  Features slipped in
&lt;/h4&gt;

&lt;p&gt;We had 1-2 teams that opted to hold off on converting their section of the app to pop off a few things they had on their plate. This squished them at the end of the project. We ended up adding 3-4 more experienced React Native engineers to the teams to hit the deadline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The data
&lt;/h3&gt;

&lt;p&gt;It’s been a few months since we’ve launched and now it’s time to present the outcomes! Let’s cycle back to the goals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The app is easier and more productive to develop consumer-grade experiences in. We fast followed with a slew of new features such as our &lt;a href="https://productupdates.getjobber.com/14561-locate-your-appointments-with-the-new-map-view" rel="noopener noreferrer"&gt;new map view&lt;/a&gt;, &lt;a href="https://productupdates.getjobber.com/13924-create-and-save-text-notes-while-offline" rel="noopener noreferrer"&gt;offline mode&lt;/a&gt;, &lt;a href="https://productupdates.getjobber.com/16187-consistently-meet-client-expectations-with-arrival-windows" rel="noopener noreferrer"&gt;arrival windows&lt;/a&gt; and &lt;a href="https://productupdates.getjobber.com/" rel="noopener noreferrer"&gt;more&lt;/a&gt;! ✅&lt;/li&gt;
&lt;li&gt;Our qualitative data from the teams saw our health of codebase, speed (team), and ease of release hit some all time highs. We saw a slight dip in the health of codebase category post-launch due to moving into different codebases unrelated to this project.
&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2bawgck6xs05khfr7yrj.png" alt="Health checks"&gt;
&lt;/li&gt;
&lt;li&gt;Our app is more reliable – it has fewer bugs, fewer crashes, fewer white screens (unhandled exceptions) ✅&lt;/li&gt;
&lt;li&gt;Holding well above 99.9% crash free sessions - a lack of observability between the previous app confounds direct comparison, but we are confident we were well below this threshold.&lt;/li&gt;
&lt;li&gt;Since launch, this has further improved from ~99.95 to ~99.98.&lt;/li&gt;
&lt;li&gt;We have improved the user experience for our SPs ✅&lt;/li&gt;
&lt;li&gt;App store&lt;/li&gt;
&lt;li&gt;All of our app store ratings are at, and remain at, an all time high

&lt;ul&gt;
&lt;li&gt;Android (Google Play) 🇺🇸 4.4 -&amp;gt; 4.8 &lt;/li&gt;
&lt;li&gt;Android (Google Play) 🇨🇦 4.1 -&amp;gt; 4.3&lt;/li&gt;
&lt;li&gt;iOS (App Store) 🇺🇸 4.6 -&amp;gt; 4.7&lt;/li&gt;
&lt;li&gt;iOS (App Store) 🇨🇦 4.5 -&amp;gt; 4.6&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.productplan.com/glossary/aarrr-framework/" rel="noopener noreferrer"&gt;AARRR metrics&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;Approved across the board (very large improvements in activation and conversion metrics)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Our calendar is more powerful ✅.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Old calendar screen&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbool.ca%2Fassets%2Fimg%2F2023%2Freact-native-migration%2Fold_calendar.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbool.ca%2Fassets%2Fimg%2F2023%2Freact-native-migration%2Fold_calendar.gif" alt="Old calendar"&gt;&lt;/a&gt;&lt;br&gt;
New calendar screen &lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvsitz5ccb2rdw9el3av8.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvsitz5ccb2rdw9el3av8.gif" alt="New calendar"&gt;&lt;/a&gt;  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Our app meets a minimum bar for performance ✅&lt;/li&gt;
&lt;li&gt;Cold starts - We’re coming for you android. We’re working to grind that down even more. Median start times from ~15s to ~5s.
&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4q4vng1u33w2c0iyhumw.png" alt="Mobile Performance"&gt;
&lt;/li&gt;
&lt;li&gt;All of the above is live by Aug 30/2022 ✅&lt;/li&gt;
&lt;li&gt;There is no ember.js left in the app by Feb 15/2022 ✅
&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fholetfd73sdvr8p545ev.png" alt="React Native Routes"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The beauty
&lt;/h3&gt;

&lt;p&gt;Before I close here’s a few more comparisons 😍. The new style and UX has received rave reviews from our customers.&lt;/p&gt;

&lt;h4&gt;
  
  
  Quote Screen
&lt;/h4&gt;

&lt;p&gt;Old quote screen&lt;br&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhx3xsq9g9q4zuwqmbb6m.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhx3xsq9g9q4zuwqmbb6m.gif" alt="Old Quote Screen"&gt;&lt;/a&gt;&lt;br&gt;
New quote screen&lt;br&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fac9pbbk5d8ufydz478pm.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fac9pbbk5d8ufydz478pm.gif" alt="New Quote Screen"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Client Screen
&lt;/h4&gt;

&lt;p&gt;Old client screen&lt;br&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsta6j6yu15c1wvme0vbs.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsta6j6yu15c1wvme0vbs.gif" alt="Old Client Screen"&gt;&lt;/a&gt;&lt;br&gt;
New client screen&lt;br&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpqtns0uttdhrg9lo7fkk.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpqtns0uttdhrg9lo7fkk.gif" alt="New Client Screen"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Visit Screen
&lt;/h4&gt;

&lt;p&gt;Old visit screen&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbool.ca%2Fassets%2Fimg%2F2023%2Freact-native-migration%2Fold_visit_screen.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbool.ca%2Fassets%2Fimg%2F2023%2Freact-native-migration%2Fold_visit_screen.gif" alt="Old Visit Screen"&gt;&lt;/a&gt;&lt;br&gt;
New visit screen&lt;br&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmryjinewxhugsx7vr6dt.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmryjinewxhugsx7vr6dt.gif" alt="New Visit Screen"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Other interesting metrics
&lt;/h2&gt;

&lt;h3&gt;
  
  
  GraphQL adoption
&lt;/h3&gt;

&lt;p&gt;GraphQL API usage/surface area increased dramatically. The data stopped when we migrated over to Datadog in Nov but we’re at almost ~80% GraphQL traffic now with our goal to deprecate the REST API by EOY. &lt;a href="https://developer.getjobber.com/apps/" rel="noopener noreferrer"&gt;Check out our public GraphQL API!&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ix4mnmld9l2tn63lril.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ix4mnmld9l2tn63lril.png" alt="REST Deprecation"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our GraphQL API covers almost all of the functionality available in Jobber’s online and mobile offering.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3d42ghebrgq86zu8y12y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3d42ghebrgq86zu8y12y.png" alt="GraphQL Types and Field Growth"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Mobile build time
&lt;/h3&gt;

&lt;p&gt;Our mobile build times were cut in half 😱&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjwfauvd0am7tc7m46oss.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjwfauvd0am7tc7m46oss.png" alt="Mobile build times"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And, we went from deploying our app every few weeks to shipping a new version every week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;I’m incredibly proud of the team and what we’ve delivered. Our customers love the new mobile app and we’ve already shipped a ton of new features in record time! Check out our &lt;a href="https://productupdates.getjobber.com/" rel="noopener noreferrer"&gt;changelog&lt;/a&gt; (Mobile App).&lt;/p&gt;

&lt;p&gt;We have a lot of future plans for Jobber’s mobile and online offering. Enjoy React? React Native? GraphQL? Rails? Helping small businesses? &lt;a href="https://getjobber.com/about/careers/#open-positions?utm_source=boolca&amp;amp;utm_medium=social&amp;amp;utm_campaign=eng_blog" rel="noopener noreferrer"&gt;Come join us at Jobber&lt;/a&gt;! We just &lt;a href="https://betakit.com/jobber-closes-100-million-usd-series-d-amid-strong-demand-for-home-services/" rel="noopener noreferrer"&gt;raised a Series D&lt;/a&gt; to grow the business and become the #1 market leader in the home services space.&lt;/p&gt;

&lt;p&gt;Thanks for reading! Want to chat? Drop a comment or shoot me an email (available in my profile).&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
    </item>
    <item>
      <title>How We Sped Up Rubocop Linting in our CI by 22x</title>
      <dc:creator>Jc</dc:creator>
      <pubDate>Fri, 12 May 2023 18:23:53 +0000</pubDate>
      <link>https://forem.com/jobber/how-we-sped-up-rubocop-linting-in-our-ci-by-22x-3cme</link>
      <guid>https://forem.com/jobber/how-we-sped-up-rubocop-linting-in-our-ci-by-22x-3cme</guid>
      <description>&lt;p&gt;At Jobber, we have been utilizing the GitHub merge queue as a way to run additional checks on code that is about to be merged - and we want this merge queue step to be fast (the target is under five minutes).&lt;/p&gt;

&lt;p&gt;We realized it would be very useful to have our Rubocop linting run in the merge queue, particularly when there were rule changes or new custom rules added. The problem is that the linting step takes nearly 7 minutes to run on our largest codebase- much too long for our merge queue target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Investigating Caching
&lt;/h2&gt;

&lt;p&gt;The way Rubocop was being invoked in CI was with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rubocop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what about caching? Without explicit management of data from previous jobs, Rubocop would be starting from scratch on every CI run. Does it support caching, and could we leverage that?&lt;/p&gt;

&lt;p&gt;It turns out that Rubocop actually &lt;a href="https://docs.rubocop.org/rubocop/usage/caching.html" rel="noopener noreferrer"&gt;has a solid caching implementation&lt;/a&gt; that takes care of all the heavy lifting, including cache invalidation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Later runs will be able to retrieve this information and present the stored information instead of inspecting the file again. This will be done if the cache for the file is still valid, which it is if there are no changes in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the contents of the inspected file&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;RuboCop configuration for the file&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the options given to rubocop, with some exceptions that have no bearing on which offenses are reported&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the Ruby version used to invoke rubocop&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;version of the rubocop program (or to be precise, anything in the source code of the invoked rubocop program)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;The cache is automatically pruned based on file count:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Each time a file has changed, its offenses will be stored under a new key in the cache. This means that the cache will continue to grow until we do something to stop it. The configuration parameter AllCops: MaxFilesInCache sets a limit, and when the number of files in the cache exceeds that limit, the oldest files will be automatically removed from the cache.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is amazing - a well thought-out cache invalidation strategy! The second point related to file changes getting stored under a new key doesn’t really help us though - the CI cache mechanism is immutable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leveraging Rubocop Caching in CI
&lt;/h2&gt;

&lt;p&gt;We can’t directly ask Rubocop what it’s going to do ahead of time (there’s no API for its caching behavior), so how do we deterministically generate a cache key for our immutable cross-workflow cache that changes in lock-step with Rubocop’s cache invalidation logic?&lt;/p&gt;

&lt;h3&gt;
  
  
  Periodic Invalidation
&lt;/h3&gt;

&lt;p&gt;Can we side-step that problem and just re-generate the cache periodically? Maybe daily, or weekly, and re-use it across all CI runs? Sure! That would certainly help - but it has the following limitations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Cache hits decrease over time as files are modified. Probably not a problem unless a large swathe of the codebase is modified within the cache period (something like a linting autofix, or a refactor / rename).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If Rubocop decides to invalidate the cache, you’ll be right back to full-length linting durations until the next cache period occurs. The most common trigger for this is a change to Rubocop configuration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The first run after each cache period will be full-length.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Shortening the cache period to mitigate some of the above issues has the side effect of increasing the amount of cache storage consumed by your project.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Intelligent Dynamic Invalidation
&lt;/h3&gt;

&lt;p&gt;What if we could integrate Rubocop’s internal cache invalidation logic with the CI’s cache invalidation logic? The limitations turn into a single bullet point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache hits decrease over time as files are modified. Probably not a problem unless a large swathe of the codebase is modified within the cache period (something like a linting autofix, or a refactor / rename).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that the CI service will typically expire a cache after a maximum number of days. In our case this happens every 15 days, and so there is a natural “reset” that catches the slow cache hit decline over time as files are modified.&lt;/p&gt;

&lt;p&gt;Here's how Jobber is powering our CI cache invalidation with Rubocop’s logic!&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Before you restore the rubocop cache directory (&lt;code&gt;~/.cache/rubocop_cache&lt;/code&gt;), lint a single dedicated file using the exact same command and configuration that the full linting step uses.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inspect what Rubocop wrote into the cache directory, and generate your cache key as a hash of that information - at this point, proceed with the normal restore, run, persist pattern.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here’s how you get the text you want to hash - assuming you used a file that is highly unlikely to change for your detection, this essentially represents a Rubocop cache key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;find ~/.cache/rubocop_cache &lt;span class="nt"&gt;-type&lt;/span&gt; f
/home/circleci/.cache/rubocop_cache/c21eac4b5c1ceb0445943396a341eadb756f46cf/7a1221dfb74d1bb683162bcc22951148cd32f1c9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output that to a file (&lt;code&gt;rubocop_cache_key&lt;/code&gt;) and hash it, combine it with other environment keys, and you get a robust cache key!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnpqcq0wf0og7ux386wza.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnpqcq0wf0og7ux386wza.png" alt="Cache key"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example cache key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rubocop-v1-{{ arch }}-ruby_&amp;lt;&amp;lt; pipeline.parameters.ruby_version &amp;gt;&amp;gt;-{{ checksum "rubocop_cache_key" }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cache Key Part&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rubocop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The descriptor of the cache key - this one is intended to be unique for rubocop purposes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A manual version number - bump this up when there’s unexpected issues and you want a straight-forward way to explicitly invalidate the cache.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{{ arch }}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CircleCI notation for the architecture, such as &lt;code&gt;arch1-linux-amd64-6_85&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ruby_&amp;lt;&amp;lt; pipeline.parameters.ruby_version &amp;gt;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The ruby version - don’t try and share caches across ruby versions. Rubocop would almost certainly invalidate the cache in this case as well, but in our case, our setup workflow detects the Ruby version and passes it onwards as a pipeline parameter so we might as well bake it in.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{{ checksum "rubocop_cache_key" }}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;This is both the “intelligent” and the “dynamic” part - it builds on the intelligent Rubocop invalidation logic, and is dynamic because this isn’t hashing text directly under source control. See the examples below for how to generate the &lt;code&gt;rubocop_cache_keyfile&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;So now we have a suitable cache key - what does it look like used in a CircleCI workflow (the following is a partial example of a CircleCI configuration file)?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;references&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;detect_rubocop_cache_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;detect_rubocop_cache_key&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Detect rubocop cache key&lt;/span&gt;
      &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec rubocop example.rb &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; find ~/.cache/rubocop_cache -type f &amp;gt; rubocop_cache_key &amp;amp;&amp;amp; cat rubocop_cache_key&lt;/span&gt;

  &lt;span class="na"&gt;restore_rubocop_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;restore_rubocop_cache&lt;/span&gt;
    &lt;span class="na"&gt;restore_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restore rubocop cache&lt;/span&gt;
      &lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;rubocop_cache_key&lt;/span&gt; &lt;span class="s"&gt;rubocop-v1-{{ arch }}-ruby_&amp;lt;&amp;lt; pipeline.parameters.ruby_version &amp;gt;&amp;gt;-{{ checksum "rubocop_cache_key" }}&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;lint_rubocop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;*bundle_install&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;*detect_rubocop_cache_key&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;*restore_rubocop_cache&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;run&lt;/span&gt;
        &lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Rubocop linting&lt;/span&gt;
        &lt;span class="s"&gt;command&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec rubocop&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;save_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Save rubocop cache&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*rubocop_cache_key&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;~/.cache/rubocop_cache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Note for Very Large Projects
&lt;/h3&gt;

&lt;p&gt;If your file count is close to 20k, you’ll want to tune &lt;code&gt;MaxFilesInCache&lt;/code&gt; to be your max file count plus a percentage to accommodate cache misses (files changing over time, between cache invalidations).&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Improvement Potential
&lt;/h2&gt;

&lt;p&gt;Once you’ve optimized the amount of work your CI is doing for linting, you can get further gains through parallelization of that work - either the multi-threading kind, or the horizontal scaling kind (both involve the same amount of work, but leveraging more hardware to complete that work faster - usually at a monetary cost).&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Improvement Results
&lt;/h2&gt;

&lt;p&gt;Before caching, linting took 476 seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffr2ku12se4qwgamdvt70.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffr2ku12se4qwgamdvt70.png" alt="Linting - before"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After caching, linting takes 22 seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fke4840x8bgdoexnedx8b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fke4840x8bgdoexnedx8b.png" alt="Linting - after"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The result (&lt;code&gt;476 / 22 = 21.6&lt;/code&gt;): &lt;strong&gt;22x faster&lt;/strong&gt; - easily fast enough to run a full linting check in our merge queue!&lt;/p&gt;

&lt;h2&gt;
  
  
  About Jobber
&lt;/h2&gt;

&lt;p&gt;Our awesome Jobber technology teams span across Payments, Infrastructure, AI/ML, Business Workflows &amp;amp; Communications. We work on cutting edge &amp;amp; modern tech stacks using React, React Native, Ruby on Rails, &amp;amp; GraphQL. &lt;/p&gt;

&lt;p&gt;If you want to be a part of a collaborative work culture, help small home service businesses scale and create a positive impact on our communities, then visit our &lt;a href="https://getjobber.com/about/careers?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=eng_blog" rel="noopener noreferrer"&gt;careers&lt;/a&gt; site to learn more!&lt;/p&gt;

</description>
      <category>performance</category>
      <category>ruby</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Custom Google Map Markers on iOS with React Native Maps</title>
      <dc:creator>Michael Davies</dc:creator>
      <pubDate>Thu, 26 Jan 2023 16:28:40 +0000</pubDate>
      <link>https://forem.com/jobber/custom-google-map-markers-on-ios-with-react-native-maps-1m2n</link>
      <guid>https://forem.com/jobber/custom-google-map-markers-on-ios-with-react-native-maps-1m2n</guid>
      <description>&lt;p&gt;&lt;em&gt;spoiler alert: It is possible! And if you are using &lt;a href="https://github.com/software-mansion/react-native-reanimated" rel="noopener noreferrer"&gt;react-native-reanimated&lt;/a&gt; in your app, it may be the culprit...&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; Preface&lt;br&gt;
&lt;strong&gt;2.&lt;/strong&gt; The Error in Question&lt;br&gt;
&lt;strong&gt;3.&lt;/strong&gt; The Broken Implementation&lt;br&gt;
&lt;strong&gt;4.&lt;/strong&gt; Discovery&lt;br&gt;
&lt;strong&gt;5.&lt;/strong&gt; Solution #1&lt;br&gt;
&lt;strong&gt;6.&lt;/strong&gt; Solution #2&lt;br&gt;
&lt;strong&gt;7.&lt;/strong&gt; Solution Trade Offs&lt;br&gt;
&lt;strong&gt;8.&lt;/strong&gt; TL;DR&lt;br&gt;
&lt;strong&gt;9.&lt;/strong&gt; About Jobber&lt;br&gt;
&lt;strong&gt;10.&lt;/strong&gt; Footnotes and References&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;NOTE: If you don't want to read this entire blog post, jump to the TL;DR.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Preface
&lt;/h2&gt;

&lt;p&gt;So, your team is creating some form of mapping software in React Native, and you've decided to leverage &lt;a href="https://github.com/react-native-maps/react-native-maps" rel="noopener noreferrer"&gt;react-native-maps&lt;/a&gt; (RNM) to seamlessly implement Google Maps (GM) on &lt;a href="https://developers.google.com/maps/documentation/ios-sdk/overview" rel="noopener noreferrer"&gt;iOS&lt;/a&gt; and &lt;a href="https://developers.google.com/maps/documentation/android-sdk/overview" rel="noopener noreferrer"&gt;Android&lt;/a&gt; applications. As with any mapping software worth its salt, you want to have custom map markers - with RNM that's easy, right? Unfortunately, you &lt;em&gt;might&lt;/em&gt; find this isn't the case.&lt;/p&gt;

&lt;p&gt;After creating a custom React Native (RN) component to use as a child of RNM's &lt;code&gt;&amp;lt;Marker /&amp;gt;&lt;/code&gt; component, you add it to your map view as per the RNM documentation, and you boot your iOS simulator to see your custom pins. Alas, there is a chance you are going to encounter this error:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Exception thrown while executing UI block: * -[__NSDictionaryM setObject:forKeyedSubscript:]: key cannot be nil&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp5de5l02gftl75311zln.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp5de5l02gftl75311zln.png" alt="A screenshot of the error on my iOS simulator" width="411" height="784"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the scenario that my team and I found ourselves in. In this blog post I will outline the two solutions we developed that enabled us to use custom pins with GM on iOS in RN, using RNM. It is worth noting that I haven't ascertained the true root cause of this issue, however, I will update this blog if that changes.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. The Error in Question
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Exception thrown while executing UI block...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Like many other software errors it's not immediately clear what the problem is. Surprisingly, a Google search of the error doesn't return many results (~94 at the time of writing). &lt;/p&gt;

&lt;p&gt;There exist a handful of unanswered Stack Overflow questions and a few issues that have been posted to the RNM GitHub repository, almost all of which invariably reference &lt;a href="https://github.com/react-native-maps/react-native-maps/issues/3983" rel="noopener noreferrer"&gt;the largest thread on this error&lt;/a&gt; I have found.&lt;/p&gt;

&lt;p&gt;Reading the limited resources on this issue, you might see answers such as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;&amp;lt;Marker /&amp;gt;&lt;/code&gt; component can't have children.&lt;/p&gt;

&lt;p&gt;You need to use Apple Maps as the provider for iOS and Google Maps as the provider for Android.&lt;/p&gt;

&lt;p&gt;The native implementation of Google Map's SDK and Apple's MapKit are incompatible, so using custom markers will cause React Native to break.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or my personal favourite, courtesy of ChatGPT:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;While using &lt;code&gt;provider={PROVIDER_GOOGLE}&lt;/code&gt;, you can not use custom pins on iOS with react-native-maps.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To generalize, it seemed as though the consensus was that you couldn't use custom pins with GM on iOS. The most commonly recommended and accepted solution is using the respective map providers for both platforms. This was something our team wanted to avoid, as to provide a consistent experience to our customers across platforms.&lt;/p&gt;

&lt;p&gt;Through failure and exploration our team found two solutions for this error&lt;sup&gt;1&lt;/sup&gt;, and I hope that by writing this I am able to contribute to the discussion and help others overcome this error&lt;sup&gt;2&lt;/sup&gt; in their own projects.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. The Broken Implementation
&lt;/h2&gt;

&lt;p&gt;To provide context, our code looked something like this when we encountered the error&lt;sup&gt;3&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; To make this easy to copy and paste for testing, I have attempted to make these code snippets as generic as possible, with limited dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our custom marker component&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CustomMarkerProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Using text as a marker for simplicity. It has the same behaviour (with respect to the error) as an SVG or Icon component.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CustomMarker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CustomMarkerProps&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;JSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Element&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Our component that uses React Native Reanimated (RNR)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For context, this component will render over the map (absolutely positioned) telling the user that there are no markers on the map. It is built with RNR. It fades in from the top of the screen, ultimately coming to rest 50 pixels below the top of the screen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ReanimatedBubble&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;JSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Element&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;animatedStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAnimatedStyle&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;withTiming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;easing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Easing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bezier&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;EASE_CUBIC_IN_OUT&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;


  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Animated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;entering&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;FadeInUp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;exiting&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;FadeInOut&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;animatedStyle&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;There are no markers on the map&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Animated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Our map view component&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;absoluteFillObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;100%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;100%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;justifyContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flex-end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alignItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;absoluteFillObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MarkerType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;JSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Element&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;markers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMarkers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;generateRandomMarkers&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateNewMarkers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setMarkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;generateRandomMarkers&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MapView&lt;/span&gt;
        &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;PROVIDER_GOOGLE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;55.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;90.32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;latitudeDelta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;12.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;longitudeDelta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &amp;gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;markers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Marker&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;coordinate&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coords&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CustomMarker&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Marker&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;MapView&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;markers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ReanimatedBubble&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"PRESS ME TO CHANGE MARKERS"&lt;/span&gt;
        &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;generateNewMarkers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// returns an array with [0, 100] markers&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateRandomMarkers&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;MarkerType&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;randInt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;markers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;randInt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;marker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;generateRandomCoords&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// returns random coordinates within Canada&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateRandomCoords&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MIN_LATITUDE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;42.33173&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_LATITUDE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;67.770579&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MIN_LONGITUDE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;125.004&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_LONGITUDE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;55.6362603&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;latitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;MIN_LATITUDE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MAX_LATITUDE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;MIN_LATITUDE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;longitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;MIN_LONGITUDE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MAX_LONGITUDE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;MIN_LONGITUDE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;coords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Using the code snippets above, on initial render of the map, we see the following on an iOS simulator:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi2cff24tpdznpywv0oh3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi2cff24tpdznpywv0oh3.png" alt="Initial custom marker implementation on iOS" width="414" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So far, so good! We have custom markers. Now, having the markers change is a critical piece of functionality.&lt;/p&gt;

&lt;p&gt;I tap the "PRESS ME TO CHANGE MARKERS BUTTON" to see new data being visualized on the map...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The application crashes, we have hit the error.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Discovery: how we approached the problem
&lt;/h2&gt;

&lt;p&gt;After encountering, then researching this error, one comment stood out to us.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You cannot render custom markers on iOS while using &lt;code&gt;provider={PROVIDER_GOOGLE}&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With the broken implementation we had, we recognized this as a rather glaring contradiction. We did render custom markers on the map's initial render, therefore it's possible. So what's going wrong?&lt;/p&gt;

&lt;p&gt;In some of the online threads, there is mention of developers running into this error only after they had started using RNR in their preexisting map applications, or after version changes of RNR.&lt;/p&gt;

&lt;p&gt;Furthermore, the error states &lt;code&gt;key cannot be nil&lt;/code&gt;. So there is somewhere in our implementation where we were dropping keys - in our case, the indices in the marker array we iterate over in the map view.&lt;/p&gt;

&lt;p&gt;These two ideas led us to two possible solutions we wanted to test:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is RNR breaking our map somehow?&lt;/li&gt;
&lt;li&gt;Can we manage the marker array in such a way that we don't drop indices, thus avoiding the error?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  5. Solution #1: RNR is breaking the map, let's fix it
&lt;/h2&gt;

&lt;p&gt;From what we had seen online, we had a hunch that RNR may be breaking our app.&lt;/p&gt;

&lt;p&gt;To confirm this, we created an empty RN project to start from scratch. We created a proof of concept map to run on iOS that  was essentially the same as what you see in the above code snippets, but without RNR or any animated components. RNM was the only library we added to the project, and the app was able to render custom pins that changed in a random fashion - confirming that it is, in fact, possible. &lt;/p&gt;

&lt;p&gt;To demonstrate it was RNR creating the conflict, we added the animated components into the app with the smallest incremental changes, rebuilding the iOS simulator after each step.&lt;/p&gt;

&lt;p&gt;If you'd like to try this, you can try the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install RNR&lt;/li&gt;
&lt;li&gt;Create the most rudimentary animated component (i.e. text wrapped in an animated view), but don't render it with your map view.&lt;/li&gt;
&lt;li&gt;Add your animated component to your map.&lt;/li&gt;
&lt;li&gt;Piece by piece, use more of the API's methods for a more complex animated component&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In doing this, we were surprised to learn that it was only specific methods within RNR that threw the error. In our case specifically, two props were to blame:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;entering={FadeInUp}&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;2. &lt;code&gt;exiting={FadeInOut}&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To remove those props while preserving the animated functionality of the "no markers" component and solve the error, we refactored as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ReanimatedBubble&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;JSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Element&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heightPadding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSharedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSharedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;animatedStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAnimatedStyle&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;withTiming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;easing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Easing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bezier&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;EASE_CUBIC_IN_OUT&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;withTiming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;easing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Easing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bezier&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;EASE_CUBIC_IN_OUT&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;heightPadding&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;


  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Animated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;animatedStyle&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;There are no markers on the map&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Animated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;On the iOS simulator with these changes, the map now works as expected!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F862gj6o52l79ofc1ahzd.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F862gj6o52l79ofc1ahzd.gif" alt="Solution 1 demo" width="1024" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Solution #2: buffer the marker array
&lt;/h2&gt;

&lt;p&gt;Given the error is complaining about keys being nil, we investigated what key(s) were becoming nil and when. Looking at our custom marker component, we're using the indices of the marker objects within the array as the &lt;code&gt;key&lt;/code&gt; prop in the &lt;code&gt;&amp;lt;Marker /&amp;gt;&lt;/code&gt; component.&lt;/p&gt;

&lt;p&gt;In testing how changes to the marker object array affects the keys being used to render the markers on the map, we discovered the following:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can render an array of &lt;code&gt;n&lt;/code&gt; objects as custom markers on the initial load of the map&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This makes sense, we knew this from the beginning!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Changing the array such that &lt;code&gt;n' &amp;gt;= n&lt;/code&gt; does not crash the app.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Again, given the error, this makes sense. In this case we aren't dropping any keys previously used for the markers. Adding additional markers isn't an issue. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Changing the array such that &lt;code&gt;n' &amp;lt; n&lt;/code&gt; crashes the app&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ah ha! This confirms that it is in fact the array indices used as keys for the markers that are crashing the app when they don't exist anymore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In other words, custom markers can change when the marker data changes, but only when the new set of markers is equal to or greater in length than the previously rendered marker array. If the new array is shorter, you will hit the error because marker indices that once existed become undefined, therefore some keys are nil.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To confirm it was the marker array indices used as keys becoming undefined we used two different methods.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;We used the timeless debugging method of console logging. When mapping over the marker array in the &lt;code&gt;&amp;lt;MapView /&amp;gt;&lt;/code&gt; we logged the indices being used. Sure enough, when the marker array decreased in length we saw undefined being console logged for missing marker objects, and the app crashed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;As a more methodical approach - we used &lt;a href="https://fbflipper.com/" rel="noopener noreferrer"&gt;Flipper&lt;/a&gt;. Adding breakpoints within the &lt;code&gt;map(() =&amp;gt; {})&lt;/code&gt; sequence, we went through the iteration step by step, looking at what data was being used. Again, in the instance of marker objects being removed from the array, we saw undefined keys and the app crashed.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;To fix this, we managed the array of marker objects such that it had a constant size.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Firstly, we made a hook that looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BUFFERED_MARKER_ARRAY_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BUFFER_MARKER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;91&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// this is not in the domain of latitudes, i.e. it won't appear on the map&lt;/span&gt;
    &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;181&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// this is not in the domain of longitudes, i.e. it won't appear on the map&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bufferedMarkerArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MARKER_ARRAY_BUFFER_SIZE&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;PLACE_HOLDER_MARKER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useBufferedMarkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MarkerType&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;MarkerType&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;markerRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bufferedMarkerArray&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;markers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;markerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;markers&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;markerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we use this hook in our &lt;code&gt;&amp;lt;MapView /&amp;gt;&lt;/code&gt; component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;absoluteFillObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;100%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;100%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;justifyContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flex-end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alignItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;StyleSheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;absoluteFillObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MarkerType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;JSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Element&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;markers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMarkers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;generateRandomMarkers&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateNewMarkers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setMarkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;generateRandomMarkers&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bufferedMarkers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useBufferedMarkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MapView&lt;/span&gt;
        &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;PROVIDER_GOOGLE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;55.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;90.32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;latitudeDelta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;12.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;longitudeDelta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &amp;gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;bufferedMarkers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Marker&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;coordinate&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coords&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CustomMarker&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Marker&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;MapView&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;markers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ReanimatedBubble&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"PRESS ME TO CHANGE MARKERS"&lt;/span&gt;
        &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;generateNewMarkers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;On the iOS simulator with these changes, the map now works as expected!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwcdbd6gb4nyy8g9v4pre.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwcdbd6gb4nyy8g9v4pre.gif" alt="solution 2 demo" width="1024" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Solution Trade Offs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Solution #1&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Removing RNR is surely a less than perfect solution for most developers. Animation is becoming increasingly important as a standard in consumer grade applications, and users now expect it.&lt;/p&gt;

&lt;p&gt;I hope to learn more about the root cause of the conflict between markers and RNR, and can hopefully update this post with a more clear answer. Better yet, this issue can probably be resolved within RNR itself, making this issue a bad memory of the past.&lt;/p&gt;

&lt;p&gt;With enough trial and error though, you should be able to find work arounds within your animated components rendered with your map to bypass this issue, while keeping those silky smooth user experiences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution #2&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let me first say that my team implemented Solution #1 within our app. We made solution 2 along with solution 1 as PoC's, to then evaluate and decide what would work best for our use case. The example code I gave for solution 2 is valid, however it has some obvious shortcomings (with respect to the code example presented within this blog):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;It has a higher time and space complexity&lt;sup&gt;4&lt;/sup&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;As written, it has a ceiling for the possible amount of marker objects.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Admittedly, I didn't bother optimizing the given example, as it serves its purpose. Some potential improvements could look something like this...&lt;/p&gt;

&lt;p&gt;Have the buffer array in the hook match the size of your marker array, such that &lt;code&gt;n_marker === n_buffer&lt;/code&gt;. If &lt;code&gt;n_marker&lt;/code&gt; ever decreases in length, maintain the longest length of &lt;code&gt;n_buffer&lt;/code&gt; by inserting buffer marker objects in place of the absent markers. By doing this, you are not limited with respect to the amount of markers you can have and you're not dropping keys. However, you will have more data persisting, which could lead to performance issues.&lt;/p&gt;

&lt;p&gt;Additionally, with the use of &lt;code&gt;useEffect()&lt;/code&gt; in the buffer hook, the data being used for the markers will exist outside of a component's typical life cycle. This again has some performance considerations, and you may want to be wary of memory leaks. To mitigate this, you could enhance the hook to manage buffer data when the map component is mounted and unmounted. Rather than digging into this here, I'll point you to &lt;a href="https://caelinsutch.medium.com/react-useeffect-hook-in-depth-dc6b7c6132e5" rel="noopener noreferrer"&gt;this blog post&lt;/a&gt; by Caelin Sutch.&lt;/p&gt;

&lt;p&gt;If you and your team implement something akin to solution 2, and it is much better than what I have outlined here, please let me know! I can update this blog post&lt;sup&gt;5&lt;/sup&gt;, or point readers to any material you may produce.&lt;/p&gt;




&lt;p&gt;Regardless of which solution you may choose to implement, if you're using custom markers you're going to want to be mindful of performance. &lt;a href="https://itnext.io/performant-custom-map-markers-for-react-native-maps-ddc8d5a1eeb0" rel="noopener noreferrer"&gt;This blog post&lt;/a&gt; by Eli Bucher covers performant map pins well, and provides a good solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. TL;DR
&lt;/h2&gt;

&lt;p&gt;If you are getting the error &lt;code&gt;Exception thrown while executing UI block: * -[__NSDictionaryM setObject:forKeyedSubscript:]: key cannot be nil&lt;/code&gt; while trying to use custom markers with Google Maps on iOS via react-native-maps, it is possible the root cause is a conflict with react-native-reanimated.&lt;/p&gt;

&lt;p&gt;I don't know &lt;em&gt;exactly&lt;/em&gt; what the conflict is, however, I have two solutions that may work for you!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution #1&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If feasible, you can remove react-native-reanimated from your project and that &lt;em&gt;should&lt;/em&gt; resolve the issue for you.&lt;/p&gt;

&lt;p&gt;If that isn't an option, I would encourage you to rebuild your map in a test project without reanimated, and implement your map so that you are using custom markers (and it should work). Then, reintroduce reanimated into that project, and add your desired functionality in piece wise until you can isolate what method in reanimated is causing the error. In our case, it was our use of &lt;code&gt;FadeInUp&lt;/code&gt; and &lt;code&gt;FadeInOut&lt;/code&gt;. Your mileage may vary, but you should be able to find alternative animation solutions while preserving your app's behaviour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution #2&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Agnostic of any react-native-reanimated conflicts, this error is thrown because the indices from the marker array you're mapping over are being dropped in the case that the marker array length, &lt;code&gt;n&lt;/code&gt;, is changing such that &lt;code&gt;n' &amp;lt; n&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;You can create a hook that will always return a constant sized array containing marker objects to your map view, so that it may map your objects to custom markers. For every index in this array, there will exist either a real marker object that renders on the map, or a buffer object that will not render on the map. Either way, the indices being used as keys will always exist, therefore you will not hit this error. &lt;/p&gt;

&lt;h2&gt;
  
  
  9. About Jobber
&lt;/h2&gt;

&lt;p&gt;Our awesome Jobber technology teams span across Payments, Infrastructure, AI/ML, Business Workflows &amp;amp; Communications. We work on cutting edge &amp;amp; modern tech stacks using React, React Native, Ruby on Rails, &amp;amp; GraphQL.&lt;/p&gt;

&lt;p&gt;If you want to be a part of a collaborative work culture, help small home service businesses scale and create a positive impact on our communities, then visit our &lt;a href="https://getjobber.com/about/careers/?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=eng_blog" rel="noopener noreferrer"&gt;careers&lt;/a&gt; site to learn more!&lt;/p&gt;




&lt;h4&gt;
  
  
  &lt;u&gt;Footnotes&lt;/u&gt;
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Take that ChatGPT, you can't steal our jobs just yet!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;All of the discussion online regarding this issue has been extremely helpful, and I recommend digging into it. While we did not find the answer we were looking for, it provided a solid foundation to dig deeper into the libraries and come up with a solution that worked for us. Thank you to everyone who has contributed to this issue!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;These examples are heavily abstracted. I attempted to demonstrate what the broken code looks like in as simple of form as possible.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Asymptotically, both time and space are in the domain of, at minimum, &lt;code&gt;2n&lt;/code&gt;, therefore a O(n) in complexity on the RNR implementation side of things, not considering the internal's of RN or any libraries used. While this doesn't seem bad, anecdotally, our team has noticed RN performance issues which I imagine would show themselves here.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And give due credit, of course!&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  &lt;u&gt;References and Links&lt;/u&gt;
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/software-mansion/react-native-reanimated" rel="noopener noreferrer"&gt;React Native Reanimated&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/react-native-maps/react-native-maps" rel="noopener noreferrer"&gt;React Native Maps&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://developers.google.com/maps/documentation/ios-sdk/overview" rel="noopener noreferrer"&gt;Google Maps iOS SDK&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://developers.google.com/maps/documentation/android-sdk/overview" rel="noopener noreferrer"&gt;Google Maps Android SDK&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/react-native-maps/react-native-maps/issues/3983" rel="noopener noreferrer"&gt;RNM - Related GitHub issue thread&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://fbflipper.com/" rel="noopener noreferrer"&gt;Flipper Debugging Tool&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://caelinsutch.medium.com/react-useeffect-hook-in-depth-dc6b7c6132e5" rel="noopener noreferrer"&gt;useEffect Blog Post&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://itnext.io/performant-custom-map-markers-for-react-native-maps-ddc8d5a1eeb0" rel="noopener noreferrer"&gt;Custom Pin Performance Blog Post&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://getjobber.com/about/careers/?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=eng_blog" rel="noopener noreferrer"&gt;Jobber Careers&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>productivity</category>
      <category>programming</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Refresh Token Rotation: What, Why and How?</title>
      <dc:creator>Taylor Noj</dc:creator>
      <pubDate>Thu, 17 Nov 2022 16:23:30 +0000</pubDate>
      <link>https://forem.com/jobber/refresh-token-rotation-what-why-and-how-2eh</link>
      <guid>https://forem.com/jobber/refresh-token-rotation-what-why-and-how-2eh</guid>
      <description>&lt;p&gt;When was the last time you connected an account you own with a third-party application or website?  Have you ever used an app marketplace?  Have you connected your GitHub account with another website? How many times have you been prompted to “allow access to” or “reauthorize” a connection?  It happens so frequently that we barely notice it anymore.  What is actually happening behind the scenes during this process? How can we as developers ensure we are safeguarding the applications we build and the people who use them?  To start answering some of those questions, let's talk briefly about authorization.&lt;br&gt;
 &lt;/p&gt;
&lt;h2&gt;
  
  
  Breaking down the authorization flow
&lt;/h2&gt;

&lt;p&gt;If you recently hit an “Allow Access” button to enable an integration or update an existing app, you’ve likely initialized an authorization flow.  The most popular authorization framework, where you are sharing information about your account with a third-party website or app, is known as OAuth 2.0.  For example, at Jobber we have an &lt;a href="https://help.getjobber.com/hc/en-us/categories/115001334127-App-Marketplace"&gt;App Marketplace&lt;/a&gt; where admin users are able to connect their Jobber accounts to third-party applications.  Much like Facebook and Amazon, Jobber also uses OAuth 2.0 to facilitate third-party authorization.  The Jobber admin user initiates a third-party app connection, grants requested permissions, and the user is directed to the app.  The client request and subsequent authorization involves scopes, which specify and limit the information the third-party application will later be able to access.&lt;/p&gt;

&lt;p&gt;But what is actually happening under the hood when you hit “Allow Access”?  For an in-depth answer to that question, I suggest exploring the &lt;a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1"&gt;Authorization Code Grant&lt;/a&gt; documentation.  To broadly summarize, once a user authorizes the client (a third-party website or app), the client receives an authorization code.  The client can then redeem this authorization code for an access token and, possibly, a refresh token. The access token is short lived, provides access to the user's resources, and allows a client to make a request to retrieve data through an API.  Just how short lived is an access token?  This is determined by the authorization server that issues an access token with parameters such as &lt;code&gt;expires_in&lt;/code&gt;.  When the access token inevitably expires, it can no longer be used to access user data or the API.  Attempting to make an API request with an expired access token will result in a 401 Unauthorized error.  &lt;/p&gt;

&lt;p&gt;What does this mean for the user?  If the authorization code grant only resulted in an access token, the user would have to start over and initiate the authorization flow once again.  This wouldn’t be a great user experience, but remember that an access token provides access to sensitive information about the user.  The longer an access token is valid, the more opportunity for a potential leak of user data.  In certain cases, where security is paramount, having the user authorize a website or application for every visit or login could be by design.  When it’s not the intended experience, how can developers ensure that a user has to authorize the client only once?  Drumroll please. . . refresh tokens!&lt;br&gt;
 &lt;/p&gt;
&lt;h2&gt;
  
  
  What are refresh tokens and why should we use them?
&lt;/h2&gt;

&lt;p&gt;Refresh tokens are long-lived credentials that a third-party developer could use to request a new access token after it has expired.  To redeem a refresh token, a third-party integration needs to authenticate itself.  This is often done using a client identifier and client secret, which are essentially a username and password for the integration.  &lt;/p&gt;

&lt;p&gt;The ability to request for a ‘refreshed’ access token means that the user can remain logged in without repeating the authorization flow, providing a more seamless user experience.  In addition, using a refresh token to request a new access token allows for the authorization server to issue very short-lived access tokens.  Stored information should be short-lived whenever possible and an easy way to allow refresh tokens to also be short-lived, is through refresh token rotation. &lt;br&gt;
 &lt;/p&gt;
&lt;h2&gt;
  
  
  How should we be using refresh tokens?
&lt;/h2&gt;

&lt;p&gt;The specifics of how refresh token rotation is implemented can vary, but in general the rotation ensures that each time a refresh token is used to request a new access token, the authorization server will return a new access token as well as a new refresh token.  When refresh tokens are being rendered invalid more frequently, the risk of replay attacks will decrease significantly.  &lt;/p&gt;

&lt;p&gt;Third-party developers who are building with &lt;a href="https://developer.getjobber.com"&gt;Jobber’s Developer Center&lt;/a&gt; have the option to enable refresh token rotation.  This feature can be selected while creating or when modifying an app.  To provide a bit more context, when developers use a refresh token to request a new access token, they are using the following POST request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /api/oauth/token HTTP/1.1
Host: api.getjobber.com
Content-Type: application/x-www-form-urlencoded

client_id=CLIENT_ID&amp;amp;client_secret=CLIENT_SECRET&amp;amp;grant_type=refresh_token&amp;amp;refresh_token=REFRESH_TOKEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resources returned are, of course, the new access token and a refresh token.  The refresh token will be either the same as before or new, depending on the client's rotation selection. &lt;/p&gt;

&lt;p&gt;It is worth noting that there could be instances where the refresh tokens can expire.  This may happen when a user has to re-authorize the client after a third-party developer adds additional scopes, or when the admin user chooses to disconnect from the client.  This list of possibilities is not exhaustive, however it’s important to know that in cases such as these, the user will need to start the authorization process again if they wish to continue using the integration.&lt;br&gt;
 &lt;/p&gt;

&lt;p&gt;Let’s revisit the authorization flow once again through a Jobber lens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An admin user decides to connect to a third-party application through Jobber’s App Marketplace.&lt;/li&gt;
&lt;li&gt;The authorization flow begins once the Jobber admin user has been redirected to an authorization screen.  In this authorization screen, the client is requesting authorization from the admin user who can in turn, approve the request by clicking “Allow Access”.&lt;/li&gt;
&lt;li&gt;The client receives an authorization code and then requests an access token and refresh token from the authorization server.&lt;/li&gt;
&lt;li&gt;The authorization server returns an access token and a refresh token.

&lt;ul&gt;
&lt;li&gt;The access token expires after 60 minutes.&lt;/li&gt;
&lt;li&gt;If refresh token rotation is disabled, the refresh token is long-lived.  For any subsequent redemption of a refresh token for an access token, the original refresh token is returned.&lt;/li&gt;
&lt;li&gt;If refresh token rotation is enabled, a completely new refresh token will be issued each time an access token is requested.
 &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The frequency in which we take part in an authorization flow in our day-to-day lives is so high that rarely do we take a minute to ask ourselves, how does this integration actually work?  I hope this article serves as a quick introduction into authorization and sparks your curiosity to &lt;a href="https://www.rfc-editor.org/rfc/rfc6749"&gt;dig deeper&lt;/a&gt;.  I also hope that if you are a developer working on an app marketplace or building a third-party application, that you strongly consider utilizing refresh token rotation.  Whenever possible, stored information should have a short lifetime and refresh token rotation is an easy way to improve the security of your applications.&lt;br&gt;
 &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About Jobber&lt;/strong&gt;&lt;br&gt;
Our awesome Jobber technology teams span across Payments, Infrastructure, AI/ML, Business Workflows &amp;amp; Communications. We work on cutting edge &amp;amp; modern tech stacks using React, React Native, Ruby on Rails, &amp;amp; GraphQL. &lt;/p&gt;

&lt;p&gt;If you want to be a part of a collaborative work culture, help small home service businesses scale and create a positive impact on our communities, then visit our &lt;a href="https://getjobber.com/about/careers?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=eng_blog"&gt;careers&lt;/a&gt; site to learn more!&lt;/p&gt;

</description>
      <category>refreshtoken</category>
      <category>authorization</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Transitioning from the Healthcare Industry</title>
      <dc:creator>Kyle Liang</dc:creator>
      <pubDate>Tue, 08 Nov 2022 17:29:30 +0000</pubDate>
      <link>https://forem.com/jobber/transitioning-from-the-healthcare-industry-52f4</link>
      <guid>https://forem.com/jobber/transitioning-from-the-healthcare-industry-52f4</guid>
      <description>&lt;p&gt;I’m Kyle Liang and I pivoted from a career in the healthcare industry to a role as a Software Engineering Intern at Jobber. In between leaving the healthcare industry and starting the internship, I completed a 3-month Web Development Bootcamp Program. This article outlines my transition from research in healthcare to software at Jobber. I started out working with digital health research and operations, helping support technology-enabled digital health programs and small-to-medium sized enterprises to bring their digital health technologies to the market. While working with these technologies, I grew interested in the technical details. During the COVID-19 pandemic, I decided to transition to a career in software development and took the opportunity to do a web development bootcamp with Lighthouse Labs Inc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web Development Bootcamp&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Prior to the bootcamp, I had never done any software development which made diving into an intensive bootcamp program with no prior experience terrifying. The program was 12 weeks of full time coding, during which we learned popular languages and frameworks like JavaScript and React. Along with those fundamentals technical skills, the bootcamp instructors mentored us in building working software, career management, and technical interview training. Even with all of that preparation, by the end of the bootcamp, I still doubted myself. With only 3 months of coding experience, how could I compete against someone with a 4 year degree in computer science?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Job Hunting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even before the bootcamp ended, I began hunting for jobs. I applied to countless companies and was often rejected for my lack of experience. I eventually came across a posting that welcomed bootcamp students: Jobber’s Software Engineer Internship Program. Jobber’s wonderful Talent Acquisition team took me through the full interview process of a phone call, technical assessment, interview, and offer to join the internship program. I never felt lost or unsure of what to expect from Jobber’s interview process, which was the best I’ve experienced in any industry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Software Engineer Internship Program at Jobber&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Jobber welcomed us with an excellent onboarding process and software engineer training. However, I still experienced Imposter Syndrome. I felt like I didn’t have enough experience or knowledge and was nervous to take on any challenging work at the beginning of the internship. Despite my initial concerns, my incredible team supported and mentored me to make sure I gained the confidence that I needed.&lt;/p&gt;

&lt;p&gt;Since my internship started, I have worked with 2 scrum teams and have contributed to Jobber’s communications system, its online application, and its new mobile application. My favourite part of the internship so far has been helping people in small businesses succeed while growing as a software engineer. At Jobber, there are always ways to contribute to the product, and endless support, mentorship, and learning opportunities to foster your development throughout the internship. &lt;/p&gt;

&lt;p&gt;One of the most impactful pieces of advice that I’ve received during my internship was to not be afraid to raise your hand and volunteer for work, even if you are uncertain about whether it is too difficult. At Jobber, your team is there to help you navigate through any challenges and provide guidance when needed. A supportive environment like this has provided me with the confidence to take on new challenges and get the most out of my internship.&lt;/p&gt;

&lt;p&gt;I am grateful to contribute to Jobber’s product and to help small businesses succeed. You are truly able to feel like you are making an impact for the small businesses using Jobber. Even though we may not get to interact with the service providers directly, their feedback lets us know that our hard work is having an impact. I sincerely appreciate Jobber for providing this incredible experience and for giving people like me who have minimal software experience an opportunity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I had a lot of doubt and uncertainty quitting my job in the healthcare industry to pursue a career in software. I was lucky enough to have the support I needed at every step of the transition, from before I quit my job, to during the bootcamp and my job hunt, to when I first started working at Jobber, and even now. Despite my initial concerns, making the jump to software development is one of the best decisions I have ever made. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About Jobber&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our awesome Jobber technology teams span across Payments, Infrastructure, AI/ML, Business Workflows &amp;amp; Communications. We work on cutting edge &amp;amp; modern tech stacks using React, React Native, Ruby on Rails, &amp;amp; GraphQL. &lt;/p&gt;

&lt;p&gt;If you want to be a part of a collaborative work culture, help small home service businesses scale and create a positive impact on our communities, then visit our &lt;a href="https://getjobber.com/about/careers?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=eng_blog"&gt;careers&lt;/a&gt; site to learn more!&lt;/p&gt;

</description>
      <category>careerchange</category>
    </item>
    <item>
      <title>The GraphQL N+1 Problem and SQL Window Functions</title>
      <dc:creator>Nick Boers</dc:creator>
      <pubDate>Fri, 07 Oct 2022 19:40:36 +0000</pubDate>
      <link>https://forem.com/jobber/the-graphql-n1-problem-and-sql-window-functions-i63</link>
      <guid>https://forem.com/jobber/the-graphql-n1-problem-and-sql-window-functions-i63</guid>
      <description>&lt;p&gt;A post by Clinton Pahl and Nick Boers, PhD&lt;/p&gt;

&lt;h2&gt;
  
  
  Table Of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;AssociationLoader and its Drawbacks&lt;/li&gt;
&lt;li&gt;Background for the SQL Window Loader&lt;/li&gt;
&lt;li&gt;Aggregate Functions&lt;/li&gt;
&lt;li&gt;Window Functions&lt;/li&gt;
&lt;li&gt;Memory-Efficient N+1 Resolution&lt;/li&gt;
&lt;li&gt;Introducing the WindowLoader&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;At Jobber, we're constantly evolving a modern &lt;a href="https://graphql.org/"&gt;GraphQL&lt;/a&gt; API to support our Web-based interface, mobile app interfaces, and third-party integrations. GraphQL allows these clients to specify the field structure and data they need in response to API queries. For example, consider the following GraphQL query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;JobVisits&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;visits&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this GraphQL query, the client has requested the title for each job visit. The response will contain an array of jobs, and for each job, an array of visits with titles. The structure of the JSON response is similar to the query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"jobs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"nodes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"visits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"nodes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Initial Assessment of Property"&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our Rails application, we use the popular &lt;a href="https://graphql-ruby.org/"&gt;graphql&lt;/a&gt; Ruby gem to resolve GraphQL queries. When used naively, it essentially resolves queries as a depth-first tree traversal, which leads to the N+1 problem in GraphQL.&lt;/p&gt;

&lt;p&gt;GraphQL’s N+1 problem, which might be better thought of as the 1+N problem, refers to the number of fetches from a backend data store necessary to resolve a relationship. In the previous example, a single fetch can obtain all of the jobs for an account. After obtaining all of the jobs, a naive resolver fetches the visits for the first job, the visits for the second job, and so on. After one fetch to get all the jobs, N additional fetches get the visits. Given the depth of relationships possible in a GraphQL query, these fetches from the backend data store can quickly balloon and lead to poor performance.&lt;/p&gt;

&lt;p&gt;The poor performance will specifically be seen in API response times. Let’s assume the above query returned 100 jobs and that fetching the visits for each job from the database takes 2 ms. In this example, the additional 100 fetches will add 200 ms to the response time for just &lt;strong&gt;one&lt;/strong&gt; field.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;AssociationLoader&lt;/code&gt; and its Drawbacks &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;For the relationships subject to the N+1 problem, the &lt;a href="https://github.com/Shopify/graphql-batch"&gt;graphql-batch&lt;/a&gt; Ruby gem and its &lt;code&gt;AssociationLoader&lt;/code&gt; provide some relief. This gem was developed by Shopify. Using Ruby promises (provided by the &lt;a href="https://rubygems.org/gems/promise.rb"&gt;promise.rb&lt;/a&gt; Ruby gem), it fundamentally alters the order of field resolution for GraphQL queries, and in a sense, converts the query from a depth-first traversal to a breadth-first traversal.&lt;/p&gt;

&lt;p&gt;As a breadth-first traversal, a call to resolve &lt;code&gt;visits&lt;/code&gt; for a single job doesn’t actually fetch data for the job’s visits. Instead, it returns a promise. The resolver returns a promise for each call to resolve visits. Once they're all batched, the data can be fetched from the backend in a single operation.&lt;/p&gt;

&lt;p&gt;Under the hood, the &lt;code&gt;AssociationLoader&lt;/code&gt; leverages &lt;code&gt;::ActiveRecord::Associations::Preloader&lt;/code&gt;. Resolving a field with promises involves collecting all of the records (e.g., jobs) where an association (e.g., visits) needs to be resolved. The Active Record &lt;code&gt;Preloader&lt;/code&gt; then goes ahead and fetches all of the data in a single data fetch operation. After loading the data, the individual promises are fulfilled using the Active Record data loaded into memory.&lt;/p&gt;

&lt;p&gt;Using the &lt;code&gt;AssociationLoader&lt;/code&gt; and GraphQL Ruby &lt;a href="https://graphql-ruby.org/type_definitions/field_extensions.html"&gt;field extensions&lt;/a&gt;, developers can easily configure a field to preload the required associations when it's resolved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:visits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;VisitType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;preload: :visits&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, this approach has a weakness as soon as a GraphQL query includes pagination arguments. Consider the following slightly more complicated GraphQL query, which obtains the first three visits for each job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FirstThreeJobVisits&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;visits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, the Active Record &lt;code&gt;Preloader&lt;/code&gt; can still be used to satisfy the &lt;code&gt;visits&lt;/code&gt; association for each job. Suppose &lt;code&gt;jobs&lt;/code&gt; contains all of the jobs, resolving &lt;code&gt;visits&lt;/code&gt; in a single fetch might involve a call like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Associations&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Preloader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:visits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, this code will fetch all of the visits for each job, even though the GraphQL response will only include the first three visits for each job. In this approach, the Rails application will need to perform the pagination, and in the process, it fetches unnecessary data from the database, which consume unnecessary memory in the Rails application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background for the SQL Window Loader &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Our initial solution to preloading was based on the &lt;code&gt;AssociationLoader&lt;/code&gt;, and it served us well to a point.  It addressed the GraphQL N+1 problem, and it significantly reduced our API response times when compared to not addressing the N+1 problem. Unfortunately, as we increased the use of connection types (with their &lt;code&gt;first&lt;/code&gt;, &lt;code&gt;last&lt;/code&gt;, &lt;code&gt;before&lt;/code&gt;, and &lt;code&gt;after&lt;/code&gt; arguments), retrieving all associated records from the database only to paginate them in the Rails application was inefficient. For objects with many associated records, it consumes more memory than necessary.&lt;/p&gt;

&lt;p&gt;After recognizing the problem, we brainstormed options to offload some of the work onto the database server to ultimately reduce the Rails application’s memory consumption. One particularly promising avenue involved SQL window functions. After deciding to pursue SQL window functions, we started our work by considering the &lt;code&gt;WindowKeyLoader&lt;/code&gt; &lt;a href="https://github.com/Shopify/graphql-batch/blob/master/examples/window_key_loader.rb"&gt;example&lt;/a&gt; described in the &lt;a href="https://github.com/Shopify/graphql-batch"&gt;graphql-batch&lt;/a&gt; repository.&lt;/p&gt;

&lt;p&gt;Given that many readers may be unfamiliar with SQL window functions, the following subsections provide some background.&lt;/p&gt;

&lt;h3&gt;
  
  
  Aggregate Functions &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;An understanding of SQL aggregate functions will help in understanding SQL window functions. Consider  a traditional SQL aggregate function such as &lt;code&gt;count()&lt;/code&gt;. In a statement involving such an aggregate function, the &lt;code&gt;GROUP BY&lt;/code&gt; clause groups records, each distinct group becomes a row in the result. The database management system (DBMS) applies the aggregate function to each group’s records to produce the function’s output.&lt;/p&gt;

&lt;p&gt;For example, consider the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
  &lt;span class="n"&gt;account_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;selects records from the &lt;code&gt;users&lt;/code&gt; table,&lt;/li&gt;
&lt;li&gt;groups those records by &lt;code&gt;account_id&lt;/code&gt; and essentially flattens them so each row in the result has a distinct &lt;code&gt;account_id&lt;/code&gt;, and&lt;/li&gt;
&lt;li&gt;computes the field &lt;code&gt;user_count&lt;/code&gt; for each row in the result by applying the function &lt;code&gt;count()&lt;/code&gt; to the records in each group.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That (simplified) explanation of the &lt;code&gt;GROUP BY&lt;/code&gt; clause provides some background for understanding &lt;u&gt;window&lt;/u&gt; functions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Window Functions &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;When using a window function, records are conceptually grouped only for the context of the function. Those groups are &lt;u&gt;not&lt;/u&gt; flattened in the result. The &lt;code&gt;OVER&lt;/code&gt; clause immediately following the function name will apply the function to a window, and the &lt;code&gt;OVER&lt;/code&gt; clause itself defines the groups of that window.&lt;/p&gt;

&lt;p&gt;For example, consider the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;login_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="n"&gt;partition_by_account_id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_rank&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="n"&gt;users&lt;/span&gt; 
&lt;span class="k"&gt;WINDOW&lt;/span&gt; &lt;span class="n"&gt;partition_by_account_id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
    &lt;span class="n"&gt;login_count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, the DBMS applies &lt;code&gt;rank()&lt;/code&gt; to subsets of the records, where &lt;code&gt;PARTITION BY account_id&lt;/code&gt; defines the subsets. For each unique account ID, the result includes a field &lt;code&gt;user_rank&lt;/code&gt; with values from 1 to &lt;em&gt;n&lt;/em&gt; where &lt;em&gt;n&lt;/em&gt; is the number of users for the unique account ID. The users are ranked by the number of times they have logged into the system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory-Efficient N+1 Resolution &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Recall the following GraphQL query from earlier, which obtains the first three visits for each job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FirstThreeJobVisits&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;visits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If jobs typically have very few visits, the &lt;code&gt;AssociationLoader&lt;/code&gt; might be a reasonable solution. If jobs have many visits, it would load many visits from the database that would ultimately be discarded because of the &lt;code&gt;first: 3&lt;/code&gt; filter.&lt;/p&gt;

&lt;p&gt;Using SQL window functions, it’s possible to apply the &lt;code&gt;first: 3&lt;/code&gt; filter at the query level and load only the necessary records into the Rails application. For example, the following query would only load the first 3 visits for each job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;numbered_visits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;row_number&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="n"&gt;partition_by_job_id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;row_number&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;
    &lt;span class="n"&gt;visits&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt;
    &lt;span class="n"&gt;job_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt;
  &lt;span class="k"&gt;WINDOW&lt;/span&gt; &lt;span class="n"&gt;partition_by_job_id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;numbered_visits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="n"&gt;row_number&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For direct associations (i.e., those where a foreign key links two tables), deriving this SQL is a rather mechanical process. It’s mechanical enough that we created a new &lt;code&gt;WindowLoader&lt;/code&gt; to make use of SQL window functions for resolving these associations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing the &lt;code&gt;WindowLoader&lt;/code&gt; &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;When data access patterns suggest an SQL window function will improve the performance resolving a field, simply adding our new &lt;code&gt;window_load&lt;/code&gt; argument to the GraphQL &lt;code&gt;field&lt;/code&gt; &lt;a href="https://graphql-ruby.org/fields/introduction.html"&gt;method&lt;/a&gt; will cause the resolver to use SQL window functions when resolving the field. The new &lt;code&gt;window_load&lt;/code&gt; argument provides the name of the association, e.g.,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;GraphqlSchema&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Main&lt;/span&gt;
    &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Types&lt;/span&gt;
      &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JobType&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;GraphqlSchema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Common&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BaseObject&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;   
        &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:visits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;VisitType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;window_load: :visits&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our &lt;code&gt;BaseField&lt;/code&gt;, derived from the graphql Ruby gem’s &lt;code&gt;Schema::Field&lt;/code&gt;, the initializer accepts this &lt;code&gt;window_load&lt;/code&gt; argument. When the argument specifies an association, the &lt;code&gt;BaseField&lt;/code&gt; constructor adds a custom connection extension to the field (&lt;code&gt;WindowConnectionExtension&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Our &lt;code&gt;WindowConnectionExtension&lt;/code&gt; class inherits from the gem’s &lt;code&gt;Schema::Field::ConnectionExtension&lt;/code&gt; and &lt;code&gt;Schema::FieldExtension&lt;/code&gt; classes. This connection extension has two hooks that wrap field resolution: &lt;code&gt;resolve&lt;/code&gt; and &lt;code&gt;after_resolve&lt;/code&gt;. The former hook is called to resolve the field, and in this instance, it uses our &lt;code&gt;WindowLoader&lt;/code&gt; class to obtain a Ruby promise for the resolution of the field. The latter hook is called after field resolution and after the resolution of promises, and in this instance, it uses our &lt;code&gt;WindowConnection&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;The former class, &lt;code&gt;GraphqlSchema::Common::Loaders::WindowLoader&lt;/code&gt;, which inherits from &lt;code&gt;GraphQL::Batch::Loader&lt;/code&gt;, first records the foreign keys that will need to be used in the SQL query. In response to &lt;code&gt;.load&lt;/code&gt; calls, it returns promises. To finally resolve the promises, it generates and runs a single SQL query that uses the previously-described window functions. Most developers using this window loader are totally unaware these steps occur behind the scenes.&lt;/p&gt;

&lt;p&gt;The latter class, &lt;code&gt;GraphqlSchema::Common::Pagination::WindowConnection&lt;/code&gt;, which inherits from &lt;code&gt;GraphQL::Pagination::ArrayConnection&lt;/code&gt;, produces a result with the expected fields for pagination, e.g., cursors and total counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Naively using the graphql Ruby gem to resolve GraphQL queries for a Ruby on Rails API leads to an implementation that suffers from GraphQL’s N+1 problem. Iterating on that solution with a gem such as graphql-batch with its &lt;code&gt;AssociationLoader&lt;/code&gt; can dramatically improve the situation by solving the N+1 problem and significantly reducing API response times. When a GraphQL query accepts arguments for pagination, a solution like the &lt;code&gt;AssociationLoader&lt;/code&gt; can lead to loading more data than necessary from the database, and as a result, higher than necessary memory consumption in the Rails server. With SQL window functions, it’s possible to offload the pagination onto the database server so that the Rails application does not receive more records than necessary. Given the flexibility of the graphql and graphql-batch gems, it’s possible to create an easy to use interface for loading data using SQL window functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  About Jobber
&lt;/h2&gt;

&lt;p&gt;Our awesome Jobber technology teams span across Payments, Infrastructure, AI/ML, Business Workflows &amp;amp; Communications. We work on cutting edge &amp;amp; modern tech stacks using React, React Native, Ruby on Rails, &amp;amp; GraphQL. &lt;/p&gt;

&lt;p&gt;If you want to be a part of a collaborative work culture, help small home service businesses scale and create a positive impact on our communities, then visit our &lt;a href="https://getjobber.com/about/careers?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=eng_blog"&gt;careers&lt;/a&gt; site to learn more!&lt;/p&gt;

</description>
      <category>graphql</category>
      <category>ruby</category>
      <category>rails</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
