Skip to content

Introduction to Remix

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

What is Remix?

Remix is a full stack web framework based on web fundamentals and modern UX. It was created by the Remix.run team, founded by Ryan Florence and Michael Jackson. They are the creators of React Router.

Remix is a seamless server and browser runtime. It has no static site support, and always relies on a server. Remix aims to provide fast page load times and instant UI transitions.

Remix is built on the Web Fetch API, which allows it to run anywhere. It can be deployed in a serverless environment or in a Node.js server environment.

Remix features

Fast data fetching

Fetching data is so fast with Remix that there is no need for transitional spinners.

Rather than fetching data via a series of requests from components, Remix will load data in parallel on the server. It will then send the browser an HTML document that contains the data, ready to be displayed.

Making use of its own cache, Remix makes page reloads really fast. Remix will reload unchanged data from the cache, and only fetch new data.

Forms

Remix has a Form component, which is an enhanced HTML form component. Remix's Form component requires no onChange, onClick, or onSubmit events on the form or its fields. Contrary to traditional React forms, there is also no need for useState fields per form input.

Remix's Form component will automatically do a POST request to the current page route with all the form's data submitted. It can be configured to do PUT and DELETE requests as well. To handle requests from a form, a simple action method is needed.

Forms have traditionally been a point of frustration with React apps. Remix's approach to forms allows developers to create forms without having to write lines and lines of boilerplate code.

Routing

Similar to Next.js, Remix uses the file system to define page routes.

Remix is built on top of React Router v6. This means that all the React Router APIs can be used in a Remix application. Anything that works with React Router will work within Remix.

When navigating using a Link tag, the Outlet tag from React Router will automatically render the link's content on the page. This makes it easy to build a hierarchy of nested routes.

Nested routes

Nested routes allow Remix to make apps really fast. Remix will only load the nested routes that change. Remix will also only update the single nested component that was updated by some user interaction.

Nested Routes also provide nested CSS styling. This allows CSS to be loaded on a per page basis. When a user navigates away from a certain page, that page's stylesheet is removed.

Error handling

When a route in a Remix app throws an error in its action method, loader, or component, it will be caught automatically. Remix won't try to render the component. It will render the route's ErrorBoundary instead. An ErrorBoundary is a special component that handles runtime errors. It's almost like a configurable try/catch block.

If a given route has no ErrorBoundary, then the error that occured will bubble up to the routes above until it reaches the ErrorBoundary of the App component in the root.tsx file (assuming TypeScript is used).

Error handling is built into Remix to make it easier to do. If an error is thrown on the client side or on the server side, the error boundary will be displayed instead of the default component. This graceful degradation improves the user experience.

Project setup

Creating a new Remix project is as easy as using the following command in a terminal window.

npx create-remix@latest

You'll be asked to pick a folder name for your app. You'll also be asked where you want to deploy. Select Remix App server.

Remix can be deployed in several environments. The "Remix App Server" is a Node.js server based on Express. It's the simplest option to get up and running with Remix.

You'll then be asked to choose between TypeScript or JavaScript. The last step will ask you if you want to run npm install. Say yes.

Once installed, Remix can be run locally by using the following terminal commands.

cd [PROJECT_FOLDER_NAME]
npm run dev

Once Remix is running, you can go to localhost:3000 in your browser to see if the default Remix installation worked. You should see the following.

remix

Project structure

Let's explore the project structure that Remix created for us.

  • The public/ folder is for static assets such as images and fonts.
  • The app/ folder contains the Remix app's code.
  • The app/root.tsx file contains the root component for the app.
  • The app/entry.client.tsx file runs when the app loads in the browser. It hydrates React components.
  • The app/entry.server.tsx generates a HTTP response when rendering on the server. It will run when a request hits the server. Remix will handle loading the necessary data and we must handle the response. By default, this file is used to render the React app to a string/stream that is sent as a response to the client.
  • The app/routes/ folder is where route modules go. Remix uses the files in this folder to create the URL routes for the app based on the naming of these files.
  • The app/styles/ folder is where CSS goes. Remix supports route-based stylesheets. Stylesheets that are named after routes will be used for those routes. Nested routes can add their own stylesheets to the page. Remix automatically prefetches, loads, and unloads stylesheets based on the current route.
  • The remix.config.js file is used to set various configuration options for Remix.

Any file ending with .client.* or .server.* is only available on the client or the server.

Demos

The default installation of Remix comes with demos that allow us to see some of Remix's defining characteristics in action.

Forms and actions

Head over to http://localhost:3000 in your browser. Click on the Actions link under the Demos In This App heading. This will give you a chance to try out Remix forms and their corresponding action methods. To view the code for this demo, take a look at the app/routes/demos/actions.tsx file.

export default function ActionsDemo() {
   // route component
}

// action is called on the server for POST, PUT, PATCH, DELETE
export let action: ActionFunction = async ({ request }) => {
  let formData = await request.formData();
  let answer = formData.get("answer");

  if (typeof answer !== "string") {
    return json("Come on, at least try!", { status: 400 });
  }

  if (answer !== "egg") {
    return json(`Sorry, ${answer} is not right.`, { status: 400 });
  }

  // Redirect after a successful action 
  // prevents the reposting of data if the user clicks back
  return redirect("/demos/correct");
};

The action function above is a server-only function to handle data mutations. If a non-GET request is made to the page's route (POST, PUT, PATCH, DELETE), then the action function is called before the loader function. Actions are very similar to loaders. The only difference is when they are called.

CSS and Nested routes

Click on the Remix logo to go back to the welcome page. Now, click on Nested Routes, CSS loading/unloading to see how Remix allows certain CSS rules to only be included on specific routes and their children. To view the code for this demo, take a look at the files in theapp/routes/demos/about/ folder, as well as the app/styles/demos/about.css file for the route-based CSS.

Linking to a nested page is as simple as:

<Link to="whoa">A nested About route</Link>

Linking back to a parent route from a nested route is as simple as:

<Link to="..">Go back</Link>

Routing and Error Boundaries

Click on the Remix logo to go back to the welcome page. Now, click on URL Params and Error Boundaries to see how routing and error boundaries work in Remix.

To view the code for this demo, take a look at theapp/routes/demos/params/$id.tsx file.

Loader

This route defines a loader function that is called on the server before providing data to the route. When a route needs to fetch data from the server, Remix uses the loader function to handle that responsibility. The loader function aims to simplify the task of loading data into components. Contrary to Next.js, API routes are not needed to fetch data for route components in Remix.

Here is the loader function above the ParamDemo component.

export let loader: LoaderFunction = async ({ params }) => {
  if (params.id === "this-record-does-not-exist") {
    throw new Response("Not Found", { status: 404 });
  }

  // when the user just isn't authorized to see it.
  if (params.id === "shh-its-a-secret") {
    // json() is a Response helper for sending JSON responses
    throw json({ webmasterEmail: "hello@remix.run" }, { status: 401 });
  }

  // simulate an exception since lol() does not exist
  if (params.id === "kaboom") {
    lol();
  }

  // return the data to the component
  return { param: params.id };
};

export default function ParamDemo() {
  let data = useLoaderData();
  return (
    <h1>
      The param is <i style={{ color: "red" }}>{data.param}</i>
    </h1>
  );
}

The ParamDemo component uses a useLoaderData hook to get the data from the loader function. In this case, that data is the id value of the URL parameter that is passed to the route.

Error Boundary

export function ErrorBoundary({ error }: { error: Error }) {  
  return (
    <>
      <h2>Error!</h2>
      <p>{error.message}</p>
    </>
  );
}

An ErrorBoundary component is rendered when an error occurs anywhere on the route. Error boundaries are helpful for uncaught exceptions that we don't expect to happen. Clicking on the This one will throw an error link will trigger the ErrorBoundary.

Catch Boundary

export function CatchBoundary() {
  let caught = useCatch();

  let message: React.ReactNode;
  switch (caught.status) {
    case 401:
      message = (
        <p>
          Looks like you tried to visit a page that you do not have access to.
          Maybe ask the webmaster ({caught.data.webmasterEmail}) for access.
        </p>
      );
      break;
    case 404:
      message = (
        <p>Looks like you tried to visit a page that does not exist.</p>
      );
      break;
    default:
      message = (
        <p>
          There was a problem with your request!
          <br />
          {caught.status} {caught.statusText}
        </p>
      );
  }

  return (
    <>
      <h2>Oops!</h2>
      <p>{message}</p>
    </>
  );
}

A CatchBoundary component is rendered when the loader throws a Response. The status code of the response can be checked from within the CatchBoundary by using the useCatch hook. Clicking on the This will be a 404 and And this will be 401 Unauthorized links will trigger the CatchBoundary.

Conclusion

Remix is backed by some of the most talented engineers in the React community. Version 1.0 of this full stack React-based web framework was just released on November 22, 2021. It's now released under the MIT license and is open source, making it free to use.

For more Remix tutorials, check out the following:

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

The Future of Dates in JavaScript: Introducing Temporal cover image

The Future of Dates in JavaScript: Introducing Temporal

The Future of Dates in JavaScript: Introducing Temporal What is Temporaal? Temporal is a proposal currently at stage 3 of the TC39 process. It's expected to revolutionize how we handle dates in JavaScript, which has always been a challenging aspect of the language. But what does it mean that it's at stage 3 of the process? * The specification is complete * It has been reviewed * It's unlikely to change significantly at this point Key Features of Temporal Temporal introduces a new global object with a fresh API. Here are some important things to know about Temporal: 1. All Temporal objects are immutable 2. They're represented in local calendar systems, but can be converted 3. Time values use 24-hour clocks 4. Leap seconds aren't represented Why Do We Need Temporal? The current Date object in JavaScript has several limitations: * No support for time zones other than the user's local time and UTC * Date objects can be mutated * Unpredictable behavior * No support for calendars other than Gregorian * Daylight savings time issues While some of these have workarounds, not all can be fixed with the current Date implementation. Let's see some useful examples where Temporal will improve our lives: Some Examples Creating a day without a time zone is impossible using Date, it also adds time beyond the date. Temporal introduces PlainDate to overcome this. ` But what if we want to add timezone information? Then we have ZonedDateTime for this purpose. The timezone must be added in this case, as it also allows a lot of flexibility when creating dates. ` Temporal is very useful when manipulating and displaying the dates in different time zones. ` Let's try some more things that are currently difficult or lead to unexpected behavior using the Date object. Operations like adding days or minutes can lead to inconsistent results. However, Temporal makes these operations easier and consistent. ` Another interesting feature of Temporal is the concept of Duration, which is the difference between two time points. We can use these durations, along with dates, for arithmetic operations involving dates and times. Note that Durations are serialized using the ISO 8601 duration format ` Temporal Objects We've already seen some of the objects that Temporal exposes. Here's a more comprehensive list. * Temporal * Temporal.Duration` * Temporal.Instant * Temporal.Now * Temporal.PlainDate * Temporal.PlainDateTime * Temporal.PlainMonthDay * Temporal.PlainTime * Temporal.PlainYearMonth * Temporal.ZonedDateTime Try Temporal Today If you want to test Temporal now, there's a polyfill available. You can install it using: ` Note that this doesn't install a global Temporal object as expected in the final release, but it provides most of the Temporal implementation for testing purposes. Conclusion Working with dates in JavaScript has always been a bit of a mess. Between weird quirks in the Date object, juggling time zones, and trying to do simple things like “add a day,” it’s way too easy to introduce bugs. Temporal is finally fixing that. It gives us a clear, consistent, and powerful way to work with dates and times. If you’ve ever struggled with JavaScript dates (and who hasn’t?), Temporal is definitely worth checking out....

The Importance of a Scientific Mindset in Software Engineering: Part 2 (Debugging) cover image

The Importance of a Scientific Mindset in Software Engineering: Part 2 (Debugging)

The Importance of a Scientific Mindset in Software Engineering: Part 2 (Debugging) In the first part of my series on the importance of a scientific mindset in software engineering, we explored how the principles of the scientific method can help us evaluate sources and make informed decisions. Now, we will focus on how these principles can help us tackle one of the most crucial and challenging tasks in software engineering: debugging. In software engineering, debugging is often viewed as an art - an intuitive skill honed through experience and trial and error. In a way, it is - the same as a GP, even a very evidence-based one, will likely diagnose most of their patients based on their experience and intuition and not research scientific literature every time; a software engineer will often rely on their experience and intuition to identify and fix common bugs. However, an internist faced with a complex case will likely not be able to rely on their intuition alone and must apply the scientific method to diagnose the patient. Similarly, a software engineer can benefit from using the scientific method to identify and fix the problem when faced with a complex bug. From that perspective, treating engineering challenges like scientific inquiries can transform the way we tackle problems. Rather than resorting to guesswork or gut feelings, we can apply the principles of the scientific method—forming hypotheses, designing controlled experiments, gathering and evaluating evidence—to identify and eliminate bugs systematically. This approach, sometimes referred to as "scientific debugging," reframes debugging from a haphazard process into a structured, disciplined practice. It encourages us to be skeptical, methodical, and transparent in our reasoning. For instance, as Andreas Zeller notes in the book _Why Programs Fail_, the key aspect of scientific debugging is its explicitness: Using the scientific method, you make your assumptions and reasoning explicit, allowing you to understand your assumptions and often reveals hidden clues that can lead to the root cause of the problem on hand. Note: If you'd like to read an excerpt from the book, you can find it on Embedded.com. Scientific Debugging At its core, scientific debugging applies the principles of the scientific method to the process of finding and fixing software defects. Rather than attempting random fixes or relying on intuition, it encourages engineers to move systematically, guided by data, hypotheses, and controlled experimentation. By adopting debugging as a rigorous inquiry, we can reduce guesswork, speed up the resolution process, and ensure that our fixes are based on solid evidence. Just as a scientist begins with a well-defined research question, a software engineer starts by identifying the specific symptom or error condition. For instance, if our users report inconsistencies in the data they see across different parts of the application, our research question could be: _"Under what conditions does the application display outdated or incorrect user data?"_ From there, we can follow a structured debugging process that mirrors the scientific method: - 1. Observe and Define the Problem: First, we need to clearly state the bug's symptoms and the environment in which it occurs. We should isolate whether the issue is deterministic or intermittent and identify any known triggers if possible. Such a structured definition serves as the groundwork for further investigation. - 2. Formulate a Hypothesis: A hypothesis in debugging is a testable explanation for the observed behavior. For instance, you might hypothesize: _"The data inconsistency occurs because a caching layer is serving stale data when certain user profiles are updated."_ The key is that this explanation must be falsifiable; if experiments don't support the hypothesis, it must be refined or discarded. - 3. Collect Evidence and Data: Evidence often includes logs, system metrics, error messages, and runtime traces. Similar to reviewing primary sources in academic research, treat your raw debugging data as crucial evidence. Evaluating these data points can reveal patterns. In our example, such patterns could be whether the bug correlates with specific caching mechanisms, increased memory usage, or database query latency. During this step, it's essential to approach data critically, just as you would analyze the quality and credibility of sources in a research literature review. Don't forget that even logs can be misleading, incomplete, or even incorrect, so cross-referencing multiple sources is key. - 4. Design and Run Experiments: Design minimal, controlled tests to confirm or refute your hypothesis. In our example, you may try disabling or shortening the cache's time-to-live (TTL) to see if more recent data is displayed correctly. By manipulating one variable at a time - such as cache invalidation intervals - you gain clearer insights into causation. Tools such as profilers, debuggers, or specialized test harnesses can help isolate factors and gather precise measurements. - 5. Analyze Results and Refine Hypotheses: If the experiment's outcome doesn't align with your hypothesis, treat it as a stepping stone, not a dead end. Adjust your explanation, form a new hypothesis, or consider additional variables (for example, whether certain API calls bypass caching). Each iteration should bring you closer to a better understanding of the bug's root cause. Remember, the goal is not to prove an initial guess right but to arrive at a verifiable explanation. - 6. Implement and Verify the Fix: Once you're confident in the identified cause, you can implement the fix. Verification doesn't stop at deployment - re-test under the same conditions and, if possible, beyond them. By confirming the fix in a controlled manner, you ensure that the solution is backed by evidence rather than wishful thinking. - Personally, I consider implementing end-to-end tests (e.g., with Playwright) that reproduce the bug and verify the fix to be a crucial part of this step. This both ensures that the bug doesn't reappear in the future due to changes in the codebase and avoids possible imprecisions of manual testing. Now, we can explore these steps in more detail, highlighting how the scientific method can guide us through the debugging process. Establishing Clear Debugging Questions (Formulating a Hypothesis) A hypothesis is a proposed explanation for a phenomenon that can be tested through experimentation. In a debugging context, that phenomenon is the bug or issue you're trying to resolve. Having a clear, falsifiable statement that you can prove or disprove ensures that you stay focused on the real problem rather than jumping haphazardly between possible causes. A properly formulated hypothesis lets you design precise experiments to evaluate whether your explanation holds true. To formulate a hypothesis effectively, you can follow these steps: 1. Clearly Identify the Symptom(s) Before forming any hypothesis, pin down the specific issue users are experiencing. For instance: - "Users intermittently see outdated profile information after updating their accounts." - "Some newly created user profiles don't reflect changes in certain parts of the application." Having a well-defined problem statement keeps your hypothesis focused on the actual issue. Just like a research question in science, the clarity of your symptom definition directly influences the quality of your hypothesis. 2. Draft a Tentative Explanation Next, convert your symptom into a statement that describes a _possible root cause_, such as: - "Data inconsistency occurs because the caching layer isn't invalidating or refreshing user data properly when profiles are updated." - "Stale data is displayed because the cache timeout is too long under certain load conditions." This step makes your assumption about the root cause explicit. As with the scientific method, your hypothesis should be something you can test and either confirm or refute with data or experimentation. 3. Ensure Falsifiability A valid hypothesis must be falsifiable - meaning it can be proven _wrong_. You'll struggle to design meaningful experiments if a hypothesis is too vague or broad. For example: - Not Falsifiable: "Occasionally, the application just shows weird data." - Falsifiable: "Users see stale data when the cache is not invalidated within 30 seconds of profile updates." Making your hypothesis specific enough to fail a test will pave the way for more precise debugging. 4. Align with Available Evidence Match your hypothesis to what you already know - logs, stack traces, metrics, and user reports. For example: - If logs reveal that cache invalidation events aren't firing, form a hypothesis explaining why those events fail or never occur. - If metrics show that data served from the cache is older than the configured TTL, hypothesize about how or why the TTL is being ignored. If your current explanation contradicts existing data, refine your hypothesis until it fits. 5. Plan for Controlled Tests Once you have a testable hypothesis, figure out how you'll attempt to _disprove_ it. This might involve: - Reproducing the environment: Set up a staging/local system that closely mimics production. For instance with the same cache layer configurations. - Varying one condition at a time: For example, only adjust cache invalidation policies or TTLs and then observe how data freshness changes. - Monitoring metrics: In our example, such monitoring would involve tracking user profile updates, cache hits/misses, and response times. These metrics should lead to confirming or rejecting your explanation. These plans become your blueprint for experiments in further debugging stages. Collecting and Evaluating Evidence After formulating a clear, testable hypothesis, the next crucial step is to gather data that can either support or refute it. This mirrors how scientists collect observations in a literature review or initial experiments. 1. Identify "Primary Sources" (Logs, Stack Traces, Code History): - Logs and Stack Traces: These are your direct pieces of evidence - treat them like raw experimental data. For instance, look closely at timestamps, caching-related events (e.g., invalidation triggers), and any error messages related to stale reads. - Code History: Look for related changes in your source control, e.g. using Git bisect. In our example, we would look for changes to caching mechanisms or references to cache libraries in commits, which could pinpoint when the inconsistency was introduced. Sometimes, reverting a commit that altered cache settings helps confirm whether the bug originated there. 2. Corroborate with "Secondary Sources" (Documentation, Q&A Forums): - Documentation: Check official docs for known behavior or configuration details that might differ from your assumptions. - Community Knowledge: Similar issues reported on GitHub or StackOverflow may reveal known pitfalls in a library you're using. 3. Assess Data Quality and Relevance: - Look for Patterns: For instance, does stale data appear only after certain update frequencies or at specific times of day? - Check Environmental Factors: For instance, does the bug happen only with particular deployment setups, container configurations, or memory constraints? - Watch Out for Biases: Avoid seeking only the data that confirms your hypothesis. Look for contradictory logs or metrics that might point to other root causes. You keep your hypothesis grounded in real-world system behavior by treating logs, stack traces, and code history as primary data - akin to raw experimental results. This evidence-first approach reduces guesswork and guides more precise experiments. Designing and Running Experiments With a hypothesis in hand and evidence gathered, it's time to test it through controlled experiments - much like scientists isolate variables to verify or debunk an explanation. 1. Set Up a Reproducible Environment: - Testing Environments: Replicate production conditions as closely as possible. In our example, that would involve ensuring the same caching configuration, library versions, and relevant data sets are in place. - Version Control Branches: Use a dedicated branch to experiment with different settings or configuration, e.g., cache invalidation strategies. This streamlines reverting changes if needed. 2. Control Variables One at a Time: - For instance, if you suspect data inconsistency is tied to cache invalidation events, first adjust only the invalidation timeout and re-test. - Or, if concurrency could be a factor (e.g., multiple requests updating user data simultaneously), test different concurrency levels to see if stale data issues become more pronounced. 3. Measure and Record Outcomes: - Automated Tests: Tests provide a great way to formalize and verify your assumptions. For instance, you could develop tests that intentionally update user profiles and check if the displayed data matches the latest state. - Monitoring Tools: Monitor relevant metrics before, during, and after each experiment. In our example, we might want to track cache hit rates, TTL durations, and query times. - Repeat Trials: Consistency across multiple runs boosts confidence in your findings. 4. Validate Against a Baseline: - If baseline tests manifest normal behavior, but your experimental changes manifest the bug, you've isolated the variable causing the issue. E.g. if the baseline tests show that data is consistently fresh under normal caching conditions but your experimental changes cause stale data. - Conversely, if your change eliminates the buggy behavior, it supports your hypothesis - e.g. that the cache configuration was the root cause. Each experiment outcome is a data point supporting or contradicting your hypothesis. Over time, these data points guide you toward the true cause. Analyzing Results and Iterating In scientific debugging, an unexpected result isn't a failure - it's valuable feedback that brings you closer to the right explanation. 1. Compare Outcomes to the hypothesis. For instance: - Did user data stay consistent after you reduced the cache TTL or fixed invalidation logic? - Did logs show caching events firing as expected, or did they reveal unexpected errors? - Are there only partial improvements that suggest multiple overlapping issues? 2. Incorporate Unexpected Observations: - Sometimes, debugging uncovers side effects - e.g. performance bottlenecks exposed by more frequent cache invalidations. Note these for future work. - If your hypothesis is disproven, revise it. For example, the cache may only be part of the problem, and a separate load balancer setting also needs attention. 3. Avoid Confirmation Bias: - Don't dismiss contrary data. For instance, if you see evidence that updates are fresh in some modules but stale in others, you may have found a more nuanced root cause (e.g., partial cache invalidation). - Consider other credible explanations if your teammates propose them. Test those with the same rigor. 4. Decide If You Need More Data: - If results aren't conclusive, add deeper instrumentation or enable debug modes to capture more detailed logs. - For production-only issues, implement distributed tracing or sampling logs to diagnose real-world usage patterns. 5. Document Each Iteration: - Record the results of each experiment, including any unexpected findings or new hypotheses that arise. - Through iterative experimentation and analysis, each cycle refines your understanding. By letting evidence shape your hypothesis, you ensure that your final conclusion aligns with reality. Implementing and Verifying the Fix Once you've identified the likely culprit - say, a misconfigured or missing cache invalidation policy - the next step is to implement a fix and verify its resilience. 1. Implementing the Change: - Scoped Changes: Adjust just the component pinpointed in your experiments. Avoid large-scale refactoring that might introduce other issues. - Code Reviews: Peer reviews can catch overlooked logic gaps or confirm that your changes align with best practices. 2. Regression Testing: - Re-run the same experiments that initially exposed the issue. In our stale data example, confirm that the data remains fresh under various conditions. - Conduct broader tests - like integration or end-to-end tests - to ensure no new bugs are introduced. 3. Monitoring in Production: - Even with positive test results, real-world scenarios can differ. Monitor logs and metrics (e.g. cache hit rates, user error reports) closely post-deployment. - If the buggy behavior reappears, revisit your hypothesis or consider additional factors, such as unpredicted user behavior. 4. Benchmarking and Performance Checks (If Relevant): - When making changes that affect the frequency of certain processes - such as how often a cache is refreshed - be sure to measure the performance impact. Verify you meet any latency or resource usage requirements. - Keep an eye on the trade-offs: For instance, more frequent cache invalidations might solve stale data but could also raise system load. By systematically verifying your fix - similar to confirming experimental results in research - you ensure that you've addressed the true cause and maintained overall software stability. Documenting the Debugging Process Good science relies on transparency, and so does effective debugging. Thorough documentation guarantees your findings are reproducible and valuable to future team members. 1. Record Your Hypothesis and Experiments: - Keep a concise log of your main hypothesis, the tests you performed, and the outcomes. - A simple markdown file within the repo can capture critical insights without being cumbersome. 2. Highlight Key Evidence and Observations: - Note the logs or metrics that were most instrumental - e.g., seeing repeated stale cache hits 10 minutes after updates. - Document any edge cases discovered along the way. 3. List Follow-Up Actions or Potential Risks: - If you discover additional issues - like memory spikes from more frequent invalidation - note them for future sprints. - Identify parts of the code that might need deeper testing or refactoring to prevent similar issues. 4. Share with Your Team: - Publish your debugging report on an internal wiki or ticket system. A well-documented troubleshooting narrative helps educate other developers. - Encouraging open discussion of the debugging process fosters a culture of continuous learning and collaboration. By paralleling scientific publication practices in your documentation, you establish a knowledge base to guide future debugging efforts and accelerate collective problem-solving. Conclusion Debugging can be as much a rigorous, methodical exercise as an art shaped by intuition and experience. By adopting the principles of scientific inquiry - forming hypotheses, designing controlled experiments, gathering evidence, and transparently documenting your process - you make your debugging approach both systematic and repeatable. The explicitness and structure of scientific debugging offer several benefits: - Better Root-Cause Discovery: Structured, hypothesis-driven debugging sheds light on the _true_ underlying factors causing defects rather than simply masking symptoms. - Informed Decisions: Data and evidence lead the way, minimizing guesswork and reducing the chance of reintroducing similar issues. - Knowledge Sharing: As in scientific research, detailed documentation of methods and outcomes helps others learn from your process and fosters a collaborative culture. Ultimately, whether you are diagnosing an intermittent crash or chasing elusive performance bottlenecks, scientific debugging brings clarity and objectivity to your workflow. By aligning your debugging practices with the scientific method, you build confidence in your solutions and empower your team to tackle complex software challenges with precision and reliability. But most importantly, do not get discouraged by the number of rigorous steps outlined above or by the fact you won't always manage to follow them all religiously. Debugging is a complex and often frustrating process, and it's okay to rely on your intuition and experience when needed. Feel free to adapt the debugging process to your needs and constraints, and as long as you keep the scientific mindset at heart, you'll be on the right track....

Next.js Conf 2021 Highlights cover image

Next.js Conf 2021 Highlights

Given how well-presented it was, it's hard to believe that Next.js Conf 2021 was only the second edition of Vercel's Next.js Conf. Vercel is the creator of the industry-leading React and JavaScript framework for full-stack web development called Next.js. This year also marked a milestone birthday celebration for Next.js as it turned 5 years old. Guillermo Rauch, CEO of Vercel and co-creator of Next.js, started off the conference with a keynote talk. Guillermo announced Next.js version 12, the biggest release ever. Guillermo highlighted the following features of Next.js 12. A new Rust-based compiler Next.js 12 uses a Rust compiler built with SWC, which stands for Speedy Web Compiler. It's an open platform for fast tooling. This compiler improves both local and production environments. This new Rust-based compiler achieves around 5 times faster builds and around 3 times faster Hot Module Replacement (or HMR). Zero code updates are required to benefit from these improvements. Simply update your Next.js app to version 12 to begin enjoying the benefits of this new compiler. ES Modules Next.js 11.1 added experimental support for ES modules. In Next.js 12, ES modules are now the default. ES modules are the official, standardized module system for JavaScript. They are supported by all major browsers and Node.js. This standardized module system allows for smaller package sizes. URL Imports Next.js 12 includes experimental support for URL Imports. This allows developers to use any package directly via a URL. There is no install or build step required. Remote resources can be processed exactly like local dependencies. Middleware & Edge Functions Over the past few years, Next.js has provided tremendous support for serverless deployments, thereby eliminating the DevOps burden for developers. Next.js continued on this mission by introducing *middleware* and *edge functions* on Vercel serverless deployments. Middleware Next.js middleware allow developers to run code before a request is completed. The response to an incoming request can be modified by rewriting, redirecting, adding headers, or streaming HTML. This allows middleware to be used for things like authentication, feature flags, A/B testing, logging, server-side analytics, and more. Next.js middleware provide the same flexibility on serverless platforms that is available with traditional web servers. Next.js middleware support Web APIs like fetch and they work out of the box in Next.js 12, or deployed as edge functions on Vercel. Edge functions Edge functions allow developers to run code close to their users. Vercel now supports edge functions for all users on their platform. It's important to note that other hosting providers can also be configured to support middleware at the edge, and not just Vercel. Multi-region deployment is often avoided due to cost and complexity. With edge functions, server-side logic can now be moved to the Edge, close to the geographic location of the website visitor. This results in improved loading times for visitors, no matter where they visit from. As developers, we typically serve dynamic content for visitor personalization reasons and serve static content for faster load times. However, with a Next.js middleware deployed as an edge function, dynamic content can be served at the speed of static content. Edge functions help to solve the common issues with serverless deployments because they boot up instantly, deploy globally, and support streaming data. The future of React Next.js 12 brings the future of React to Next.js. It includes experimental support for React 18 and React server components. It was reassuring to hear how closely Next.js is working with the React team to prepare Next.js for React 18, server-side streaming, and React server components. React server components React server components allow individual components to be rendered on the server-side. It's different from *Server-Side Rendering (SSR)*, which pre-generates the HTML of an entire web page on the server. Server-Side Rendering renders all the HTML on the page or nothing at all. With React server components, a web page can progressively update at the component level as data comes in. With React server components, data fetching can be done at the component level. This simplifies things, because getServerSideProps and getStaticProps will no longer be needed. This aligns more closely to the React hooks model, which advocates for combining data fetching with components. Using server components React server components can be used in Next.js 12 with an experimental flag. To try out React server components, we can simply give any page a .server.js or .server.ts suffix. Client-side rendered components can be used within the server component. These client components will hydrate and become interactive when the server component is rendered. This can be useful for adding client-side functionality to a page, like upvoting and downvoting behaviors. Benefits of server components React server components require no client-side JavaScript and they improve page rendering speeds, as well as the user experience. React server components provide greater flexibility, allowing the developer to choose where a component renders, either on the client or on the server. Granular component caching The Next.js team showcased a new exploration that is currently underway using a new Data component that works like a React suspense boundary. This new component supports granular component caching. It's similar to *Incremental Static Regeneration (ISR)*. Next.js Analytics Last year, Next.js announced Next.js Analytics to help monitor web vitals. Since then, it has seen amazing adoption, processing over 7 billion data points. This year, Next.js announced Checks, automated performance checks that start automatically and report results when a deployment finishes. A preview deployment is checked to ensure that there are no runtime errors and that good performance makes its way into production. This is all thanks to the end-to-end reliability checks and *Web Vitals* performance checks that are done. Next.js Live During the conference, we learned about the evolution of Next.js Live, which was presented at last year's Next.js Conf. Next.js Live provides an instant development and real-time collaboration experience for Next.js. All it takes to collaborate with Next.js Live is a link. Next.js Live makes it possible to code, chat, draw, and edit, directly from a web browser. Next.js Live now provides new Git integrations. Next.js Live runs natively in web browsers, and now boots up instantly thanks to innovations like ES Modules and Web Assembly, which the new compiler takes advantage of. Notable Talks Next.js 12 Overview Developer Experience of the Future by Lee Robinson. Learn more about the features included in Next.js 12. Edge functions Next.js at the Edge by Suzanne Aldrich and Lee Robinson. Learn more about Edge functions with Next.js and Vercel. Edge Functions Explained by Kelsey Hightower and Lee Robinson. See a demo of edge functions and learn how they're different from the traditional way of building for the web. Next.js and databases Next.js and Prisma: Databases in Serverless Made Easy by Daniel Norman. Learn how Prisma makes building database-driven applications with Next.js a breeze. Streaming Streaming in Next.js by Kara Erickson. Learn about the benefits of streaming with Next.js. Getting Started Getting Started with Next.js by Delba de Oliveira. Learn the fundamentals of React and Next.js. All Talks To view all the talks that were part of Next.js Conf 2021, visit the Vercel YouTube page....

Implementing Dynamic Types in Docusign Extension Apps cover image

Implementing Dynamic Types in Docusign Extension Apps

Implementing Dynamic Types in Docusign Extension Apps In our previous blog post about Docusign Extension Apps, Advanced Authentication and Onboarding Workflows with Docusign Extension Apps, we touched on how you can extend the OAuth 2 flow to build a more powerful onboarding flow for your Extension Apps. In this blog post, we will continue explaining more advanced patterns in developing Extension Apps. For that reason, we assume at least basic familiarity with how Extension Apps work and ideally some experience developing them. To give a brief recap, Docusign Extension Apps are a powerful way to embed custom logic into Docusign agreement workflows. These apps are lightweight services, typically cloud-hosted, that integrate at specific workflow extension points to perform custom actions, such as data validation, participant input collection, or interaction with third-party services. Each Extension App is configured using a manifest file. This manifest defines metadata such as the app's author, support links, and the list of extension points it uses (these are the locations in the workflow where your app's logic will be executed). The extension points that are relevant for us in the context of this blog post are GetTypeNames and GetTypeDefinitions. These are used by Docusign to retrieve the types supported by the Extension App and their definitions, and to show them in the Maestro UI. In most apps, these types are static and rarely change. However, they don't have to be. They can also be dynamic and change based on certain configurations in the target system that the Extension App is integrating with, or based on the user role assigned to the Maestro administrator on the target system. Static vs. Dynamic Types To explain the difference between static and dynamic types, we'll use the example from our previous blog post, where we integrated with an imaginary task management system called TaskVibe. In the example, our Extension App enabled agreement workflows to communicate with TaskVibe, allowing tasks to be read, created, and updated. Our first approach to implementing the GetTypeNames and GetTypeDefinitions endpoints for the TaskVibe Extension App might look like the following. The GetTypeNames endpoint returns a single record named task: ` Given the type name task, the GetTypeDefinitions endpoint would return the following definition for that type: ` As noted in the Docusign documentation, this endpoint must return a Concerto schema representing the type. For clarity, we've omitted most of the Concerto-specific properties. The above declaration states that we have a task type, and this type has properties that correspond to task fields in TaskVibe, such as record ID, title, description, assignee, and so on. The type definition and its properties, as described above, are static and they never change. A TaskVibe task will always have the same properties, and these are essentially set in stone. Now, imagine a scenario where TaskVibe supports custom properties that are also project-dependent. One project in TaskVibe might follow a typical agile workflow with sprints, and the project manager might want a "Sprint" field in every task within that project. Another project might use a Kanban workflow, where the project manager wants a status field with values like "Backlog," "ToDo," and so on. With static types, we would need to return every possible field from any project as part of the GetTypeDefinitions response, and this introduces new challenges. For example, we might be dealing with hundreds of custom field types, and showing them in the Maestro UI might be too overwhelming for the Maestro administrator. Or we might be returning fields that are simply not usable by the Maestro administrator because they relate to projects the administrator doesn't have access to in TaskVibe. With dynamic types, however, we can support this level of customization. Implementing Dynamic Types When Docusign sends a request to the GetTypeNames endpoint and the types are dynamic, the Extension App has a bit more work than before. As we've mentioned earlier, we can no longer return a generic task type. Instead, we need to look into each of the TaskVibe projects the user has access to, and return the tasks as they are represented under each project, with all the custom fields. (Determining access can usually be done by making a query to a user information endpoint on the target system using the same OAuth 2 token used for other calls.) Once we find the task definitions on TaskVibe, we then need to return them in the response of GetTypeNames, where each type corresponds to a task for the given project. This is a big difference from static types, where we would only return a single, generic task. For example: ` The key point here is that we are now returning one type per task in a TaskVibe project. You can think of this as having a separate class for each type of task, in object-oriented lingo. The type name can be any string you choose, but it needs to be unique in the list, and it needs to contain the minimum information necessary to be able to distinguish it from other task definitions in the list. In our case, we've decided to form the ID by concatenating the string "task_" with the ID of the project on TaskVibe. The implementation of the GetTypeDefinitions endpoint needs to: 1. Extract the project ID from the requested type name. 1. Using the project ID, retrieve the task definition from TaskVibe for that project. This definition specifies which fields are present on the project's tasks, including all custom fields. 1. Once the fields are retrieved, map them to the properties of the Concerto schema. The resulting JSON could look like this (again, many of the Concerto properties have been omitted for clarity): ` Now, type definitions are fully dynamic and project-dependent. Caching of Type Definitions on Docusign Docusign maintains a cache of type definitions after an initial connection. This means that changes made to your integration (particularly when using dynamic types) might not be immediately visible in the Maestro UI. To ensure users see the latest data, it's useful to inform them that they may need to refresh their Docusign connection in the App Center UI if new fields are added to their integrated system (like TaskVibe). As an example, a newly added custom field on a TaskVibe project wouldn't be reflected until this refresh occurs. Conclusion In this blog post, we've explored how to leverage dynamic types within Docusign Extension Apps to create more flexible integrations with external systems. While static types offer simplicity, they can be constraining when working with external systems that offer a high level of customization. We hope that this blog post provides you with some ideas on how you can tackle similar problems in your Extension Apps....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co