Remixing How We Give

Bookmark

A review of how we're using Remix at Daffy.org to change the way people give to charities.


We'll talk about why we decided to use Remix, how we've used it and migrated from our previous frontend application and some patterns and libraries we have developed internally.

by



Transcription


Hello, welcome to my presentation. I'm Sergio Salambre. I am a web developer at Daffy and I'm going to talk how we use Daffy, how we use Remix at Daffy. So what is Daffy? Daffy is a platform to help people donate to the charities they care about in an automated way. To do that, we provide both an iOS application and a web application and we use Remix for that web application. We also use Remix for other apps, but we are going to focus on how we use it for main web that web users use. So first, how we started using and building the front end. Daffy or MVP was a route application being served from a Rails route and compiled by Rails webpacker. It worked great for the MVP proof of concept, but then we changed it to Next.js, powering the landings and the web because we wanted to have a server-side rendering and other features. But we had some problems with that setup. We had slow pages and slow connections to the website. If there was an error on one of our API calls to get data to render on the page, the WordPress crashed and shows an expected error. Also, forms were way too complex to validate and run to errors. We have many errors there and we have some unnecessary duplication of code. Also, sharing code in getServerSideProps function is too complex because you need to grab it on higher order functions. So we decided we needed to use something else and we chose to use Remix. The reason for that is the first one, the resilience. So the resilience is how well an app can support, can keep working in case of an error. Remix doesn't make your app work all the time if there is an error, but it helps you a lot to get there or near there. With error boundaries, we can catch any unexpected error and show something to the user like, hey, something went wrong. We know about this now, but you can contact support if you need more help. With catch boundaries, we can customize how we present to users any known errors like a not found. The user goes to a charity and that charity doesn't exist, that's a not found. Or a user profile that doesn't exist, not found. They want to do something that needs more money on their account, we can return a missing payment error UI with catch boundaries. Same with lack of authorization. We also can use progress enhancement. So if JS failed to load for any reason, the application is mostly usable. At least they can still access the content. Another reason was for a mutation. With the previous stack, we had to do a lot of things to the mutation like create a form, a state for input, serialize that, send it in a fetch to an API, create an API in another file, send others authorized, validate data, and send it to our Rails API. With Remix, this becomes way simpler. We just render the form, export, and action on the same file the form lives in. We can do the server-side logic authorization and validation of the data before sending it to Rails, and it works. It's way simpler. It's half of the steps. Action call is also on the same file of the form, not another file. That's a great benefit for being able to know what's happening in a route. We go to a route, we saw there is a form, we go to the action, we know what that form is doing. We can also add validation with salt. Multistep forms can work with JavaScript way simpler and with the back button, something we had issues before. And with progress enhancement, we can use the useTransition hook to enhance the experience of the users and show from loading states to optimistic UIs. Conventions and separation of concerns is also another reason we choose Remix. Roots files help a lot with refactoring. We can just remove a root file and remove the action, the loader, the component, links, everything. We need to move code to another file and import it in many files, like headers, we just create a pattern, layout route, and put inside everything that needs that header, and that's it. And the component can live in the root. Loaders and actions also use web standards, and web standards means we spend less time on Remix docs and more time on the Mozilla developer network docs. Also means hiring and teaching the stack is simpler. We hired someone who didn't know Remix, and she learned Remix for the interview and passed the interview, and other developers focusing on backend were able to learn Remix super fast in like a day or less than a day and started using it. Now, how we migrated to Remix. We know we wanted to use Remix, but we still had this big Next application while we did. The first thing was we can migrate everything at once, so we decided to run Remix and Next together. To do this, we take advantage that Remix can be plugged in an Express server, and Next can also be plugged in an Express server, so we use Express to run both processes, both apps in a single process. We send everything from a slash underscore Next slash whatever to Next so it can handle APIs and sending assets. We use Express for the public folder and the public build folder for Remix. We send a specific request to Remix and the rest of the request to Next initially. This means we had to handle errors from Next at the beginning, but eventually we switched and we started to send all requests to Remix by default and only specific requests to Next. We did this when we moved all our cache, all routes, that is the user profile slash whatever is a user profile. So we moved that to Remix, and that allows us to do the switch and say, okay, from now on, everything goes to Remix except this part, like API route from Next or some routes that we were still serving with HTML for Next. We also had to share the authentication state because moving from one app to another for the user should have been the same. The user didn't know if the route was using Remix or Next, except probably because it was faster. So we created a mixout, we changed it out zero in Next.js SDK to Remix out with the out zero strategy. And then we realized that the session storage object from Remix is actually not tied in any way to Remix. We can use it in Next. You can install that part in the Next app and have it working. So there you can see how you can pass the reg.headers.cookie value to sessionstorage.getSession and get the string to send to res.setHeader. And in the same way we can do with Remix. So we shared this session storage to be able to share the authentication state across apps. Finally, another issue we had was Remix links pointing to Next apps while we're trying to do a client-side navigation. And that failed because the route didn't exist on Remix yet. And the same happened in the other way. Next links pointing to Remix tried to do the same, trying to load the following route the user was navigating to using Next, but that didn't exist anymore. So the solution was for the time the migration was still happening, we added reload document in Remix components and avoided using Next link component. So every link was a full-base navigation, same with forms. Every form was causing a full-base navigation. Even if the link or the form was pointing to a route we already know was using Remix. So we set the baseline basically, everything used full-base navigation. And after the migration was ready and we know there was not any more Next.js code running in production, we removed all reload documents and the app became a single-page application. Now how we use Remix at Daphne. First, Remix is a backend for frontend for us. The user interacts only with Remix. There is no way the user sees a request to our Rails API, just send it to Remix and from there to Rails. And we use this to aggregate data from multiple endpoints of our API. A server side, so on the loaders, we can just fetch multiple endpoints. We get that data and filter it also on the server side. And we can transform data. For example, we convert date objects to localize messages or numbers to localize the string amount. And because of this, because most of these things we used to do on the UI are now in the loaders, our React components are simpler because they just focus on the UI. They focus on get the data from use loader data and show it to the user. And a few specific states or efforts for integration mostly with third-party services. Also we have this idea of a root file has to contain everything they need at least as much as possible. We only move components to the app slash components folder if they are truly shared, like we use it on almost every root, or at least more than three. So we know before moving a component to this folder that the component is actually going to be used in a lot of parts, it's not tied to the data. This allows simple refactoring of roots because if everything is in the root, we can just remove the roots and remove all the related code. There is no way in where we remove a root and we left the component in the component folder because we are not sure if something else is using it. We also avoid coupling components, shared components with data because all of our shared components just receive data from props and not from user data and they will find exactly what they need. And a lot of simple components because if they are complex components, they are usually tied to what the root is specifically doing and just lives in the root itself. We also have this idea of a query function. So this is how a loader looks like in Remix. The initial code where we authenticate the user, create API clients, instances, and things like that are expected to not fail. So if they fail, we want the error boundary to render. We then try to catch everything that is expected that may fail. If there is an error, we capture the exception and send it to Sentry. And we also return or through a response to let the user know something went wrong. We are through when the whole root is bad and we return when we can still render part of it. And the query functions are these get something functions with async functions we have inside the loader at the end of the function, the loader code, where we fetch data from the API. We also filter it or parse or transform it there. So they will call it in a single function. And inside this function, we can try to catch the request. So this means if getDonation fails, in this case, we want the root to fail because if you are seeing a specific donation, nothing makes sense without that data. But if getCharities fail, we can return null and capture the exception to Sentry. So we know something failed. But for the user, what's going to happen is the list of charities disappears from the UI. So important queries cause the main type to fail inside the loader, but the others fail silently. We do something similar with mutations. So a mutation is again a function we put at the bottom of the action where we create the sort schema, we validate the form data with the sort schema and send it to the API. And if it's a success, we can return JSON with a status success or a redirect or whatever we need. And if there is an error, we check if it's a sub-error. We return JSON with a status error and a list of error messages. If it's not a sub-error, it means something else failed, we capture the exception and show a message, usually telling the user to contact support and that we already know of the error. We also handle errors on the UI this way. So we have the cache boundary in case the wall loader failed, the wall root failed. But we move a part of the UI from the root to different components. And inside those components that live in the same file as the root, we call useloader data, get the data we need and render it. And if the data is null, we just return null to hide that part of the interface. We also define custom conventions using the handle export. Most of these custom conventions are not part of RemixUtils. Things like hydrate scripts and dynamic links are a part of RemixUtils. And we have this global type, Daffy.handle, that we can attach to our handle export and define what that root can set there. So our roots define if they need or not JavaScript, if they need external scripts, can load it that way. We also extend this convention for different layouts. So the boarding handle have different conventions that attach it to the global ones. Same with layout on the app handle. And you can see, for example, in the app handle, we have this aside component that receives the loader data props. And this allows the app layout root to render the sidebar with the side, but render it in a larger layout with the root, children roots inside it in another place. So we can use this as a way to define slots or setting it in some way for different parts of the layout. We also have a type safe API client. So we don't use something like Prisma because we fetch an API. So in order to have type safety, when we fetch from our own API, we define a schema, we saw the export, infer the type from there. When we fetch the API, we just parse the data as it comes to, from, we saw, and let it fail if there is an error. So if there is an error there, we know something changed on the API that wasn't expected to change, and we can review, we review the error and check what happened. And if not, and if something worked correctly, the return value from the API methods is going to be correctly typed with a schema donation type. We also use a lot of resource routes for different things. We, for example, generate PDF because we had to give users report about the donation for tax purposes. So we use a react PDF library to render a react inside a resource route. We generate a PDF. We localize the PDF using Y18next library. And then after we render the PDF, we send a response using the PDF helper from Remix Utils. And that way we can generate dynamic PDF with react in the resource route. We also use resource routes for Open Graph images. In the same way, we just get params from the URL search params. We use react to render an SVG as a string. If the user expects an SVG response, we just return it. If not, we can transform the SVG to something else like a PNG, JPG, WebP, et cetera, and send the file with a content type header. And this way we can just optimize images for Open Graph as we need it. Now Remix at Daffy has contributed to the Remix community because a lot of things were extracted from there. Remix Utils exists thanks to Remix at Daffy. We use client-only use hydrated, use global pending state, and more things come from internal functions on components. We use it at Daffy and then we extract it to Remix Utils. Remix was born because the need to implement Auth0 authentication at Daffy. I published the first draft with my initial attempt. And then from the proof of concept we have from Remix, we created Remix Auth version 1 with the Auth0 strategy that was later extracted to another package and the library grow with the community. Remix Y18next also exists thanks to Daffy using Remix because we need to localize the app. The app is actually in English only, but we support localization already. We choose Y18next translated from them. So again, my initial implementation was published as an article and then we extracted the library with the output code. We use it as a Remix Y18next from Daffy proof of concept. That was how the library was created and the community started to contribute. And that's it. Thanks for joining me. Sergio, how are you today? I'm fine. Oh good. Well, that was an amazing talk. Thank you so much. We want to jump over to Slido and see what the results were of the poll. Let me get my screen over to that. And so we asked, have you migrated an existing project to Remix? And 53% of people said no, they haven't migrated yet, but they would like to. And then some people said yes, about 38% and then nobody, no, I don't have plans to about 6%. So very little don't want to migrate at all. But that's pretty cool that a lot of people are planning to, right? Yeah, yeah, you should. You should keep trying. So hopefully this talk helps them get the motivation to go ahead and start migrating their apps over. And don't forget that you can always join at slido.com and 1818 is the code if you want to answer on any of these polls that we're doing. All right, let's jump back into the Q&A. So we have a question that we asked the audience in the beginning and we're asking every speaker now. We would like to know what is your favorite feature of Remix? I think it's the same, the majority. It's mutations. Mutations. Yeah, it's the hardest part in any application. It's easy to read data. It's hard to mutate it correctly and revalidate and all the things Remix does around that. And it's super easy with actions and forms. Yeah, I think we might get back into that in a question later, but that is a very popular answer to that question is mutations. We hear loaders and actions a lot. And so I think that is a very popular and requested feature also. So how long did it actually take you to do the migration that you talked about? We started in January of this year. And if I'm not wrong with my memory, we completed it by September. So it took us around nine months to do the whole migration while we keep adding more features. Okay, so how did that process look like for you then with taking that time? Were you able to incrementally switch over and migrate your application? Yeah, well, we did that basically that because we are a small startup, we are still figuring out a lot of things on our product. So when we redesigned things to improve how they work, we used that opportunity to move them to Remix and build a new version, the rendering Remix. And then when we have some empty spaces of work, we didn't have anything else to do for a few days. We used that time to migrate other things that were not so important that we are not touching so much, but we have in Nexon. So we migrate them to Remix. What are some of the processes you use to do those incremental changes? Because you move from Next.js to Remix, which have two very different APIs. So how are you able to kind of mix those together? Well, what we did was to use Express, right, to run both applications on the same server using one process. And then we had to share the authentication state. After we figured that, moving things was basically everything from the ground in Remix, maybe reusing part of the UI. Luckily for us, because I really like a lot of the Remix patterns, we didn't do too much things client side, like data fetching. So we used GetServiceAprops. Moving from GetServiceAprops to Loader is not that hard as if you use another library for querying data client side. Really that's kind of an interesting point that you could just almost like find and replace your GetServerSideProps with Loader and get your data that you need for your route. I like that. Yeah, a lot of code is the same. You just need to change how to read things from the request and how to send things in the response. But the rest of the code, the actual logic you do there is the same. Awesome. Well, I guess that would make it a little bit easier. So what are some of the benefits of migrating from Remix to Remix that you've seen in your application? We have way smaller JavaScript files in the client. So our application feels faster when we are navigating with this link preload feature. Remix can preload not only the code as it does Next, it also preloads the data and other assets. So if we have images that we preloaded, it can fetch them. If we have a loader on the next route, we can also start prefetching it before the user actually clicks a link and navigate faster to the other page, even if we are doing server side rendering all the time. Yeah, so maybe it has to do with just the way that the router in Remix is set up and is able to incrementally load some of those things that you need. Yeah. Yeah, awesome. All right, when making the switch from Next.js to Remix, you mentioned that progressive enhancement is also one of the reasons. Can you explain more about that and how Remix handles the progressive enhancement? Yeah, for those who don't know, progressive enhancement is this idea that your app works without JavaScript and then you enhance it so that it works better for people with JavaScript loaded that can be most of the users usually. In our case, we work with money. And if the app doesn't work, it's worrying for the users, right? Like, you have my money there, even if you already donated when you give the money to us, it's really your money. And using progressive enhancement means if there is an error on our client side JavaScript, you can still see that and use it. A lot of things, not everything, but a lot of things still work without JavaScript, like forms and things like that. So most of the app can be used even without JavaScript. And that's how progressive enhancement is helping us. And this can be done in Remix because Remix use link to navigate and link is an angle and use the form component, which is an actual form. And everything keeps working that way. That actually makes a lot of sense too, because you're working with that money. You want to give that user feedback on what they're doing in your application. So you need to have that progressive enhancement is like, this is working behind the scenes, but you can't see it until it's actually finished. But we're going to progressively show you an optimistic update in hopes that that data is going to come back, right? Yeah. Awesome. So we have a couple of audience questions now. Chris said, did you have any issues with convincing your team to migrate to Remix? I think you said from Next.js. And how do you go about that at the start? When we started thinking, when I started thinking and convincing the team to use Remix, we were just starting the front end. We built an original proof of concept with React routers being served by Rails, your root. And then we needed to move to something that's better. At that moment, Remix was still paid. My CTO was not convinced to use it because it's paid. It means not everyone is going to know it. It's going to be harder to hire someone who knows the framework. We started using Next because of that. Also, I contributed. One of my coworkers is one of the original authors of Next.js. So we started with Next. Then we decided to migrate to Remix because we started to have issues with the performance of the application on the front end. The landing pages were great, but once you are on the application part, it's not that great, especially if you don't use most things client-side, as we did. And that was what convinced the CTO to try Remix. He tried it on the week between Christmas and New Year. After that, he came back from New Year and basically told me, yeah, Remix is great. This is my list of things. Why I like it, we are going to migrate it to Oracle to do it. It's like, if they don't want to switch over to Remix, just show them. Just make them try it. Yeah, I think that's the best way to convince someone to use it. Just try it. Show it. Awesome. So is your app a monorepo? It's not a monorepo. Well, we have a monorepo, but my application has the front end is one folder and it has everything for the front end. There is no share with other parts of the monorepo because it's other languages. Okay. We have a question that's in regards to a monorepo. So if you don't know it, don't worry. No worries. But I just wanted to ask it in case. Have you experienced difficulties importing local component packages into Remix in a monorepo environment? Yeah, we didn't try it. We just have all the code in the same application. Yeah. Awesome. So I don't have experience with that either. No. Okay. Let's do one last question from Adrian. How many people are working on the code base that you migrated? Let me count it. We have one, two, three, four, five, six, seven, eight people. Nice. So it's a fairly small team, right? It's pretty easy to convince people. I think that's something that I've always said is if you're talking to a stakeholder or somebody who's not a developer about migrating something like that, you want to make sure that you're using their terms and phrasing it in different ways than you would if you're talking to a developer. So I think that's another thing to think about if you're trying to convince your company to migrate over. Yeah. So when we started, we had four people. We hired the rest after we started the migration. And then Remix, super fast. One of them learned it for the interview in a week, like another week in two days, something like that. That's another great point is that Remix doesn't have a lot of specific Remix stuff and it uses the web platform. So you're really just learning JavaScript for the most part. And React, I guess. I think that makes hiring someone to work on Remix easier than hiring someone to work on anything else, basically, because you don't need to learn too much from it. You only learn about the web itself. Yeah. Awesome. Well, I think that is about all the time we have for questions. Thank you so much for answering these, Sergio. And thank you for joining us today. Thank you. All right. See ya.
32 min
18 Nov, 2022

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Workshops on related topic