1. Introduction to Daphne and Remix
Hello, welcome to my presentation. I'm Sergio Salambri, web developer at Daphne, and I'm going to talk how we use Daphne, how we use Remix at Daphne.
So what is Daphne? Daphne 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. We are going to focus on how we use it for main web. That's what users use.
So first of all we started using and building the front end. Daphne or MVP was a router application being served from a Rails route and compiled by Rails webpacker. It worked great for the MVP proof of concepts but then we changed it to Next.js for the landings and the web because we wanted to have a server and other features. We had some problems with the setup. We had slow pages on a slow connection to the website. If there was an error on one of our API calls to get data to render on the page, they would crash and show a suspected error. Also, forms were way too complex to validate and run to errors. We have some unnecessary duplication of code. Also sharding code in get server-side props function is too complex, because you need to grab it on high-order functions.
So we decided we needed to use something else, and we chose to use RemixBoot. And 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 are going to – we know about this now, but you can contact support if you need more help. With Cache Boundaries, we can show any known errors. The user goes to a charity and that charity doesn't exist, that's not fun. Or a user provider doesn't exist, it's not fun. They want to do something that needs more money on their account, we can return a missing payment cell or UI with Cache Boundaries. We also can use Progressive Enhancement. If a JS fails to load for any reason, the application is mostly usable. At least they can access the content. Another reason was form ammutation.
2. Simplifying Form Submission and Migration to Remix
With Remix, the process of form submission, authorization, and validation becomes much simpler. The action call is in the same file as the form, making it easier to understand the flow of the application. Remix also offers conventions and separation of concerns, allowing for easier refactoring and avoiding code duplication. Additionally, the migration to Remix was done by running Remix and Next together, utilizing the express server to run both apps in a single process.
With the previous stack, we had to do a lot of things to do ammutation like create a form, a state for input, serialize that, send it in a fetch to an API, create the API in another file, send authorize, validate data and send it to our Rails API. With Remix, this becomes way simpler. We just render the form, export in action on the same file the form lives in. We can do the service and launch authorization and validation of the data before sending it to Rails and it works. It's way simpler.
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 with that the action, the loader, the component, links, everything. We need nested roots also help avoid duplication. We don't need to move code to another file and import it into many files like headers, across roots. We just create a pattern, layout root, and put inside everything that needs that header and that's it. And the component then lives in the root. Loaders and actions also use standards. And we spend less time on Remix docs and more time on 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, 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. What 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. We use express to run both processes, both apps in a single process.
3. Using Next and Remix Together
We send requests to Next for APIs and assets, but everything else goes to Remix. We handle errors from Next initially, but eventually switched to sending all requests to Remix. We shared the authentication state across apps using a shared sessionStore. During the migration, we used full page navigation for links and forms. Now, Remix serves as the backend for our frontend, aggregating data from multiple API endpoints. Loaders handle data fetching, filtering, and transformation, simplifying our React components.
We send everything from a slash underscore Next slash whatever, to Next, so it can handle APIs and sending assets. We use express static for the public build, the public folder and the public build folder for Remix. We send a specific request to Remix and the rest of the requests 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 NextJS. 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 allowed us to do the switch and say, okay, from now on everything goes to Remix except this path, 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 the Out0 Next.js SDK to RemixOut with the Out0 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 res.headers.cookie value to sessionStore.getSession and get the session, and then commit the session and get the string to send to res.setHeader. And in the same way we can do with Remix. So we shared this sessionStore 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 root didn't exist on Remix yet. And the same happening 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 page navigation. Same with forms. Every form was causing a full page 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 page 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 they all become a single page application packet.
Now how we use Remix at Daffin. First, Remix is a backend for frontend for us. The user interacts only with Remix, there is no way the user see a request to 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. Server side, on the loaders, we can just fetch multiple endpoints. We get the data and filter it also on the server side, and we can transform data. For example, we convert date objects to localizing 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.
4. Root File Structure and Component Coupling
We have the idea of a root file containing everything needed, avoiding coupling components with data. Shared components receive data from props and find exactly what they need. Complex components are tied to the root and reside in the root itself.
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 apps slash components folder if they are truly shared, like if 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 is not tied to the data. This allows simple refactoring of roots because if everything is in the root, we can just remove the root and remove all the related code. There is no way in where we remove a root and we left the component in the component because we are not sure if something else is using it. We also avoid coupling components with data. Because all of our shared components just receive data from props and not from the data. And they will find exactly what they need. And a lot of simple components because they are not, if they are complex components, they are usually tied to what the root is specifically doing and do sleep in the root itself.
5. Query Functions, Mutations, and Error Handling
We have the concept of query functions in Remix, which are async functions that fetch data from the API and handle filtering, parsing, and transformation. These functions are placed at the end of the loader code. Errors in important queries cause the main catch to fail inside the loader, while others fail silently. We handle mutations in a similar way, validating form data with a SOD schema and sending it to the API. We return JSON responses with success or error statuses accordingly. We also handle UI errors by moving parts of the UI to separate components and rendering them based on the data availability. Additionally, we define custom conventions using the handle export and have a type-safe API client to ensure type safety when fetching from our own API.
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 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 true a response to let the user know something went wrong. We true when the word root is bad, and we return when we can still render part of it.
And the query functions are these get something functions, async functions we have inside the loader at the end of the loader code where we fetch data from the API. We also filter it or parse or transform it there so the word code is in a single function. And inside this function, we can try catch the request. So this means if you get donation 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 you get charities fail, we can return null and catch the exception to center. 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 catch to fail inside the loader but the others fail silently.
We do something similar with mutation. So mutation is again a function we put at the bottom of the action where we created the SOD schema, we validate the form data with the SOD schema and send it to the API. And if it's a success, we can return it JSON with the status success or a red or whatever we need. And if there is a random error, we check if it's a SOD error, we return in JSON with a status error and the list of error messages. If it's not a SOD error, it means something else failed, we got your reception and show a message usually telling the user to contact support that we already know of the error.
6. Resource Routes and Remix Libraries
We use resource routes to generate PDFs and open graph images. The React PDF library is used to render PDFs with localization support from Y18-next. For open graph images, we render SVGs and transform them as needed. RemixAdafi has contributed to the Remix community, leading to the creation of RemixUtils, RemixOut, and RemixY18next. These libraries provide various features and integrations. Thank you for joining me!
We also use a lot of resource routes for different things. We for example generate PDF because we have 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 and generate a PDF. We localize the PDF using Y18-next library and then after we render the PDF, we send a response using the PDF helper from RemixUtils and that way we can generate a dynamic PDF with React.
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, etc, and save the file with a content type header. This way we can just optimize images for OpenGraph as we need it.
RemixAdafi has contributed to the Remix community because a lot of things were extracted from there. RemixUtils exists. Thanks to RemixAdafi, we use client-only useHydrated, useGlobalPendingState, and more things come from internal functions and components. We use it in Adafi and then we extract it to RemixUtils. RemixOut was born because the need to implement Auth0 authentication. At Dafi, I published the first draft with my initial intent and then from the proof of concept we have from Remix. We created RemixOut version 1 with Auth0 strategy that was later extracted to another package and the library grow with the community. RemixY18next also exists thanks to Dafi using Remix because we need to localize the app. The app is actually English only but we support localization already. We choose Y18next translated from them. My initial implementation was published as an article. Then we extracted the library with the output code. We used it as Y18next from Dafi proof of concept. That was how the library was created and the community started to contribute. 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.
Migrating Existing Projects to Remix
We asked the audience if they have migrated an existing project to Remix. 53% said no, but they would like to, while 38% said yes. Only 6% have no plans to migrate. It's great to see that many people are planning to migrate. The favorite feature of Remix mentioned by the audience is mutations, which is considered the hardest part of any application. The migration process took around nine months, starting in January and completing in September. The migration was done incrementally, taking advantage of redesign opportunities and empty workspaces.
Let me get my screen over to that. 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 it up. So hopefully this talk helped them get the motivation to go ahead and start migrating their apps over. And don't forget that you can always join us slido.com and 1-818 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 is 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, 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 was around nine months to do the word 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 do 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 had some empty spaces of work, we didn't have anything else to do for a few days, use 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.
Mixing Next.js and Remix
To mix Next.js and Remix together, we used Express to run both applications on the same server. We shared the authentication state and leveraged Remix's UI patterns. Instead of heavy client-side data fetching, we used GetServerSideProps and easily transitioned to Remix's Loader. The code logic remained the same, with only minor adjustments to reading requests and sending responses.
What are some of the processes you used 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 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 reveal 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 GetServerSideProps. Moving from GetServerSideProps to Loader is not that hard as if you used another library for querying data client side. Actually, 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 use there is the same. Awesome. Well, I guess that would make it a little bit easier.
Benefits of Migrating to Remix
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.
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. It's 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? I started thinking of convincing the team to use Remix. We were just starting the front-end. We built an original proof of concept with React routers being set up by Rails in your route, and then we needed to move to something that was better.
Migrating to Remix and Monorepo Structure
We initially built a proof of concept with React routers, but eventually decided to migrate to Remix due to performance issues. The CTO was initially hesitant to use Remix because it was a paid framework and might make hiring more difficult. However, after trying Remix during the holiday season, the CTO was convinced of its benefits. Our application is not a monorepo, but rather a single folder containing all the frontend code.
We built an original proof of concept with React routers being set up by Rails in your route, and then we needed to move to something that was 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 is going to be harder to hire someone who knows the framework. We started using Next because of that. I contributed, one of my co-workers is one of the original authors of NextJS. We started with Next, then we decided to migrate to Remix because we started to have issues with the performance of the application. The frontend landing pages were great, but what you are on the application part is not that great, especially if you don't use most things client-side, and that was what combined 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. While I like it, we are going to migrate to our code, to do it. It's like if they don't want to switch over to Remix, just show them, just like make them try it, right? Yeah, I think that's the best way to combine 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 it has the front end is one folder that has everything from the front end. There's no share with other parts of the monorepo, because it's other languages.
Monorepo Importing and Team Size
Okay, we have a question that's in regards to a monorepo, so if you don't know it, 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.
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 1, 2, 3, 4, 5, 6, 7, 8 people. Nice. It's a fairly small team, right? It's pretty easy to convince people. And I think that's something that I've always said is, like, 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.