Remix is the newest trending JavaScript framework in the ecosystem right now. It is a full stack web framework that lets you focus on the user interface and work back through web standards to deliver a fast, slick, and resilient user experience. Personally, i like the fact that Remix is carrying us back to web development foundations with amazing features such as:
- Nested Routes: Remix loads data in parallel on the server and sends a fully formed HTML document. This almost eliminates loading states and makes your site super lightning fast. You might not need Skeleton UI anymore. This is good news to most frontend developers like me.
- Simple: If you know HTML, you will enjoy working with Remix. For example, you can implement a form without JavaScript. Remix runs actions server side, revalidates data client side, and even handles race conditions from resubmissions.
- Error Handling: Remix makes Error handling easy with Error boundaries. While we are still waiting for React to provide Error boundaries for functional components, Remix handles errors while Server Rendering and while Client Rendering too.
- Actions and Loaders: After Nested Routes, this is the next most amazing feature of Remix. In Remix, you use actions for mutations and loaders for retrieving data. Interesting fact about Loaders is that you can do most of your data transformation and calculations there, like check if a list is empty, limit the number of records, only send specific attributes, so your React component just receives the data and renders it, no logic needed. You can think of loaders as “GET” request handlers, the code snippet below shows how loaders work:
1import { json } from "@remix-run/node"; // or "@remix-run/cloudflare"23export const loader = async () => {4 // The `json` function converts a serializable object into a JSON response5 // All loaders must return a `Response` object.6 return json({ ok: true });7};
The loader()
function is only run on the server. Later in this article, we will be making use of loaders.
- Lightning page speed: With nested routes, instant transitions leveraging distributed systems and native browser features, built on web fetch api, and cloudfare workers, Remix could have 99 problems, but page speed ain’t one.
Cloudinary is a platform on which we can upload, store, manage, transform, and deliver images and videos for web and mobile applications. Cloudinary provides an exhaustive API for uploading media (including images, video and audio). The Upload API enables you to upload your media assets (resources) and provides a wide range of functionality, including basic and advanced asset management, metadata management, and asset generation. Chakra UI is a simple, modular and accessible component library that gives you the building blocks you need to build your React applications.
In this tutorial, we will be building a Remix app that allows us to upload images to Cloudinary.
Pre-requisites
You will need the following to follow this tutorial:
- Nodejs >=v14 installed
- Knowledge of JavaScript
- A code editor (preferably VSCode)
The complete code is on Codesandbox.
Disclaimer: Codesanbox does not support Remix out of the box yet. To view this demo on Codesandbox, fork the project, provide your cloudinary credentials, open the terminal, and run these commands
1cd my-remix-app2npm run dev
The app will be running on port 3000.
Getting Started
Let’s begin setting up our Remix project. Run this command to bootstrap a new Remix project
1#bash2npx create-remix@latest
When the script is done installing, you’ll be prompted with some questions, make sure you choose these options:
1#bash2? Where would you like to create your app? remix-upload-image3? What type of app do you want to create? Just the basics4? Where do you want to deploy? Choose Remix App Server if you're unsure; it's easy to change deployment targets. Remix App Server5? TypeScript or JavaScript? JavaScript6? Do you want me to run `npm install`? Yes
Once it’s done installing dependencies, run this command to change directory to our project directory:
1#bash2cd remix-upload-image
Open the folder with your code editor, and let’s go through the directory structure of our remix app.
1#bash2.3├── README.md4├── app5│ ├── entry.client.jsx6│ ├── entry.server.jsx7│ ├── root.jsx8│ └── routes9│ └── index.jsx10├── package-lock.json11├── package.json12├── public13│ └── favicon.ico14├── remix.config.js15└── jsconfig.json
app
: this folder is where all the Remix code goes
app/entry.client.tsx
: this is the first file that runs when the app loads in the browser
app/entry.server.tsx
: this is the first file that runs when a request is made to the server
app/root.tsx
: this is the root of our remix application. Similar to index.tsx
in Next
app/routes
: just like Nextjs, this folder handles routing and pages
public
: this is where you put static files such as images, fonts, etc
remix.config.js
: similar to next.config.js
, this has all Remix configurations
Let’s write our first Hello World program.
Navigate to app/routes
and delete index.jsx
. Go to app/root.jsx
, delete everything, and add these lines of code:
1//app/root.jsx23import { LiveReload } from "@remix-run/react";45export default function App() {67return (8 <html lang="en">9 <head>10 <meta charSet="utf-8" />11 <title>Remix Upload Images</title>12 </head>1314 <body>15 Hello Remix world16 <LiveReload />17 </body>18 </html>19);20}
Here, LiveReload
handles hot reload in Remix.
Go ahead and run npm run dev
in your terminal to serve the development build. You should get something like this:
1➜ remix-upload-image npm run dev23> dev4> remix dev56Watching Remix app in development mode...7💿 Built in 736ms8Remix App Server started at http://localhost:3000 (http://192.168.80.200:3000)
Built in 736ms!
Navigate to http://localhost:3000
in your browser and you should see your hello world program.
Let’s go ahead to implementing the cloudinary image uploading functionalities.
Implementing the Image uploader
Firstly, we’ll update our app/root.jsx
to look like this:
1#root.jsx23import {4 Links,5 LiveReload,6 Meta,7 Outlet,8 Scripts,9 ScrollRestoration,10} from "@remix-run/react";11import stylesUrl from "~/styles/global.css";1213export const links = () => {14 return [{ rel: "stylesheet", href: stylesUrl }];15 };1617export const meta = () => ({18 charset: "utf-8",19 title: "Remix Upload image",20 viewport: "width=device-width,initial-scale=1",21 });2223export default function App() {24 return (25 <html lang="en">26 <head>27 <Meta />28 <Links />29 </head>30 <body>31 <Outlet />32 <ScrollRestoration />33 <Scripts />34 <LiveReload />35 </body>36 </html>37 );38}3940export function ErrorBoundary({ error }) {41 return (42 <div className="error-container">43 <h1>App Error</h1>44 <pre>{error.message}</pre>45 </div>46 );47}
Here, we import components from the @remix-run/react
package and set an Error Boundary. Most important of those components are the <Links />
and <Meta />
. The <Link />
component handles all link
exports all through the app and the <Meta />
component handles all meta exports on all routes while <Outlet />
gives room for Children routes. Earlier, i mentioned that one of my favourite features of Remix is how it handles errors, well you can see how easy it is to create ErrorBoundaries. We have a global.css
****file referenced in our code, let’s create it now.
Create a styles/global.css
directory and add these lines of code:
1/* styles/global.css */23body {4 background-color: #000000;5 color: #ffffff;6 width: 1200px;7 margin: auto;8 font-family: sans-serif;9}1011.error-container {12 background-color: hsla(356, 77%, 59%, 0.747);13 border-radius: 0.25rem;14 padding: 0.5rem 1rem;15}16a {17 color: wheat;18 text-decoration: none;19}2021a:hover {22 text-decoration: underline;23 opacity: 0.8;24}
This will be the styles used throughout our app.
Let’s go ahead to create our Index route. Create a app/route/index.jsx
directory and add the following lines of code:
1#app/route/index.jsx23import { Link } from "@remix-run/react";45export default function Index() {6 return (7 <div>8 <h1> Remix image upload </h1>9 <p>10 This is a Remix app for uploading images to cloudinary11 </p>12 <Link to="/cloudinary-upload"> Upload Images here </Link>13 </div>14 );15}
This is going to be our /
route and page. Nothing much is going here except importing the @remix-run/react
Link. It represents the <a>
****anchor tag and it’s the main method of navigation in a remix app.
Let’s go ahead to create the main thing, the cloudinary upload route. Create a cloudinary.jsx
in the route
folder and add these lines of code:
1#app/route/cloudinary.jsx23import {4 json,5 unstable_composeUploadHandlers as composeUploadHandlers,6 unstable_createMemoryUploadHandler as createMemoryUploadHandler,7 unstable_parseMultipartFormData as parseMultipartFormData,8} from "@remix-run/node";9import { Form, useActionData } from "@remix-run/react";10import { uploadImage } from "~/utils/utils.server";11import formStylesUrl from "~/styles/form.css";1213export const links = () => {14 return [{ rel: "stylesheet", href: formStylesUrl }];15};1617export const action = async ({ request }) => {18 const uploadHandler = composeUploadHandlers(19 async ({ name, data }) => {20 if (name !== "img") {21 return undefined22 }23 const uploadedImage = await uploadImage(data)24 return uploadedImage.secure_url;25 },26 createMemoryUploadHandler()27 );2829 const formData = await parseMultipartFormData(request, uploadHandler);30 const imgSource = formData.get("img");31 const imgDescription = formData.get("description");3233 if (!imgSource) {34 return json({35 error: "something is wrong",36 });37 }38 return json({39 imgSource, imgDescription40 });41};4243export default function Index() {44 const data = useActionData();45 return (46 <>47 <Form method="post" encType="multipart/form-data" id="upload-form">48 <div>49 <label htmlFor="img"> Image: </label>50 <input id="img" type="file" name="img" accept="image/*" />51 </div>52 <div>53 <label htmlFor="description"> Image description: </label>54 <input id="description" type="text" name="description" />55 </div>56 <div>57 <button type="submit"> Upload to Cloudinary </button>58 </div>59 </Form>6061 {data?.errorMsg && <h3>{data.errorMsg}</h3>}62 {data?.imgSource && (63 <>64 <h2>Uploaded Image: </h2>65 <img src={data.imgSource} alt={data.imgDescription || "Upload result"} />66 <p>{data.imgDescription}</p>67 </>68 )}69 </>70 )71}7273export function ErrorBoundary({ error }) {74 return (75 <div className="error-container">76 <pre>{error.message}</pre>77 </div>78 );79}
Let’s break this into snippets.
Firstly, we imported some functions from the nodejs part of remix.
json
converts a response object to a JSON object,unstable_composeUploadHandlers
: this is an upload handler that accepts our HTML input field name as parameter and file bytes from the uploaded images as data.unstable_createMemoryUploadHandler
: this is another upload handler that stores streamedmultipart/form-data
parts in memory.
These functions makes up our uploadHandler
function. Before we leave the uploadHandler
let’s take a look at this code snippet:
1//javascript2const uploadedImage = await uploadImage(data)3return uploadedImage.secure_url;
The uploadImage(data)
takes in the image bytes as parameter, returns a Promise that resolves if our image has been saved to the cloudinary folder we’ll specify soon and rejects if there’s an error. If an image is uploaded successfully, it returns an object that looks like this for us to use:
1{2 asset_id: '1c80a31297c4748b7d655190d1e5023b',3 public_id: 'remixImages/nrubhrrcv030zhiulzzb',4 version: 1657796627,5 version_id: 'e78d675b8199040f471c89ce1903a8b0',6 signature: 'f50122a9819e3458630afde2927dab615f05437c',7 width: 1000,8 height: 1333,9 format: 'jpg',10 resource_type: 'image',11 created_at: '2022-07-14T11:03:47Z',12 tags: [],13 bytes: 204402,14 type: 'upload',15 etag: 'ccd7e9f2ecda9e91de437ce15a9464b5',16 placeholder: false,17 url: 'http://res.cloudinary.com/sammy365/image/upload/v1657796627/remixImages/nrubhrrcv030zhiulzzb.jpg',18 secure_url: 'https://res.cloudinary.com/sammy365/image/upload/v1657796627/remixImages/nrubhrrcv030zhiulzzb.jpg',19 folder: 'remixImages',20 original_filename: 'file',21 api_key: '<api-key>'22}
We’ll be creating the utils.server.js
very soon.
Let’s look at another code snippet here:
1const formData = await parseMultipartFormData(request, uploadHandler);2const imgSource = formData.get("img");3const imgDescription = formData.get("description");45if (!imgSource) {6 return json({7 error: "something is wrong",8 });9}10return json({11 imgSource, imgDescription12});
The unstable_parseMultipartFormData
is a remix utility that handles multipart formdata file uploads instead of using request.formData
. It returns the field value that our uploadHandler
returns. For instance, the value of imgSource
will the URL of the image uploaded to cloudinary.
action({ request })
is an async function that comes before the loader and your template.
So, how do we access the data from our actions right?
const
*data*
=
*useActionData*``();
This line of code does that. The useActionData()
hook returns the JSON parsed data from our action.
If you’ve understood through these parts, you’re awesome! Let’s create two files that we called in our route.
First, create a form.css
file in the styles
directory and add these lines of css:1#styles/form.css23#upload-form {4 width: 280px;5 margin: 5rem auto;6 background-color: #fcfcfc;7 padding: 20px 50px 40px;8 box-shadow: 1px 4px 10px 1px #aaa;9}10#upload-form * {11 box-sizing: border-box;12}13#upload-form input {14 margin-bottom: 15px;15}16#upload-form input[type=text] {17 display: block;18 height: 32px;19 padding: 6px 16px;20 width: 100%;21 border: none;22 background-color: #f3f3f3;23}24#upload-form label {25 color: #777;26 font-size: 0.8em;27}28#upload-form button[type=submit] {29 display: block;30 margin: 20px auto 0;31 width: 150px;32 height: 40px;33 border-radius: 5px;34 border: none;35 color: #eee;36 font-weight: 700;37 box-shadow: 1px 4px 10px 1px #aaa;38 background: #207cca; /* Old browsers */39 background: -moz-linear-gradient(left, #207cca 0%, #9f58a3 100%); /* FF3.6-15 */40 background: -webkit-linear-gradient(left, #207cca 0%,#9f58a3 100%); /* Chrome10-25,Safari5.1-6 */41 background: linear-gradient(to right, #207cca 0%,#9f58a3 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */42 filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#207cca', endColorstr='#9f58a3',GradientType=1 ); /* IE6-9 */43}44img {45 width: 100%;46}47input#img {48 color: red;49}
Another awesome thing about Remix is how it handles styles. When we navigate to the cloudinary-upload
it loads this form.css
file and when we leave the route, it unloads the styles. Take a second to imagine how that improves the overall site speed.
Let’s finally create the utils/util.server.js
directory. Add the following lines of code:
1// utils/util.server.js23import cloudinary from "cloudinary";4import { writeAsyncIterableToWritable } from "@remix-run/node";56cloudinary.v2.config({7 cloud_name: process.env.CLOUD_NAME,8 api_key: process.env.API_KEY,9 api_secret: process.env.API_SECRET,10});1112async function uploadImage(data) {13 const uploadPromise = new Promise(async (resolve, reject) => {14 const uploadStream = cloudinary.v2.uploader.upload_stream(15 { folder: "remixImages" },16 (error, result) => {17 if (error) {18 reject(error)19 return;20 }21 resolve(result)22 }23 )24 await writeAsyncIterableToWritable(data, uploadStream);25 });26 return uploadPromise;27}2829export { uploadImage }
This is is the server side of Remix. The file above is a server module. It’s best practice in Remix to handle all server side code in a *.server.js
module, Remix also uses "tree shaking" to remove server code from browser bundles.
One last thing, create a .env
file and add these details from your cloudinary dashboard:
1// .env2CLOUD_NAME=cloud name here3API_KEY= api key here4API_SECRET= api secret here
Now go ahead and run your development server with:
1npm run dev
You should be presented with these screens:
Go ahead to cloudinary and check our Media Uploads for the remixImage folder and our cat image.
Conclusion
In this guide, we learned about Remix and it’s top features, we also explored cloudinary and used it upload API to build a Remix app that enables users to upload images to Cloudinary.
Further Reading
Happy Coding!