Remix has an HTML first, simple data access philosophy. Most Remix web applications work without JavScript code in the browser. It is possible to create a complete dynamic data driven Remix web application with only server side JavaScript code with pure HTML in the browser.
About Remix
This back to the roots approach makes Remix applications easy to understand, extend, and troubleshoot. A single TypeScript file contains the database access (backend) and the browser side (frontend) code. The data transfer API setup between the server and the browser is handled entirely by the Remix framework, only a few lines of code is needed to access the data in the browser.
The loader() function reads the data from the database and exposes it in JSON format to the browser. We can even expose environment variables to the browser, but make not to send secret values, as those are accessible to the user.
In the browser side code we use “|| {}” to avoid TypeError: Cannot destructure property ‘incidents’ of ‘useLoaderData(…)’ as it is null.
// /app/routes/_index.tsx file
import { useLoaderData } from '@remix-run/react';
import { LoaderArgs, json } from "@remix-run/node";
// --------------------------------------
// The server side code
export async function loader({ request }: LoaderArgs) {
...
const data = await GET_THE_DATA_FROM_THE_DATABASE();
...
// Expose the data to the browser
return json({ environment: process.env.ENVIRONMENT, data: data });
}
// --------------------------------------
// The browser side code
export default function Index() {
const { environment, data } = useLoaderData<typeof loader>() || {} ;
return (
<>
Environment: {environment.environment}
</>
)
}
To create a new Remix web application
The official instructions are at Remix Quickstart
To start the development of a new Remix React.js web application
- Create a new directory for the project
- Create the .gitignore file
- Open a terminal in the project directory and initialize a new Remix project. To learn about the Remix templates visit Remix Stacks. We will use the basic structure without any template.
npx create-remix@latest
MY_PROJECT_NAME- Answer y to the question
Need to install the following packages:
create-remix@1.19.3
Ok to proceed? (y) - Select Just the basics and hit enter for the question
What type of app do you want to create?
- Select Remix App Server for the question
Where do you want to deploy?
- Select TypeScript and press enter for the question
TypeScript or JavaScript?
- Answer Y for the question
Do you want me to run
npm install
?
- Answer y to the question
Relative paths
React/Remix applications are compiled before deployment, during compilation output files are placed in the public directory. Always use relative paths when you refer to files:
For imports, the “app” directory is the root, indicated with the tilde (~). Do not specify the extension in the import line, as .ts and .tsx files will be compiled to .js.
import { logError } from './logHelper';
import { Ticket, Customer } from '~/models/types';
For images, the “public” directory is the root
<img src="header.png" style={{width: '100%'}} />
Test the Remix web application
- Open a terminal and navigate into the web application directory
- Start the web server
npm run dev
- In the web browser navigate to http://localhost:3000/
Styles
There are multiple ways to reference style sheets in React, we will combine them to be able to use a global style sheet for the overall look and feel of the site and load additional page specific sheets.
- Create the /app/styles directory for the style sheets
mkdir app/styles
- Create the /app/styles/global.css style sheet file
/* /app/styles/global.css file */
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
font-family: Roboto, Helvetica, Arial, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
/* =========================================================== */
/* BEGIN Loading indicator fade in */
.fade-in-image {
background-color: white;
animation: fadeIn 5s;
-webkit-animation: fadeIn 5s;
-moz-animation: fadeIn 5s;
-o-animation: fadeIn 5s;
-ms-animation: fadeIn 5s;
}
@media (prefers-color-scheme: dark) {
.fade-in-image {
filter: invert(100%);
}
.fade-in-image h1 {
color: black;
}
}
@keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
@-moz-keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
@-webkit-keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
@-o-keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
@-ms-keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
/* END Loading indicator fade in */
/* =========================================================== */
/* BEGIN Menu */
.navlink {
margin: 7px;
margin-left: 20px;
float: left;
color: #f0f0f0;
}
#navbar a.pending {
color: gray;
}
#navbar a.active {
color: white;
font-weight: bold;
}
/* END Menu */
/* =========================================================== */
/* BEGIN Page content */
.pagecontent {
margin-left: 20px;
margin-right: 20px;
}
/* END Page content */
- Update the /app/root.tsx file to load the global and page specific style sheets for every page. We also define the common title and favicon values for the entire site.
// /app/root.tsx file
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
// ========================================
// Import the global style sheet
import styles from "~/styles/global.css";
// Import the LinksFunction
import type { LinksFunction } from "@remix-run/node";
// Expose the imported stylesheet to the <Links /> module
export const links: LinksFunction = () => {
return [
{
rel: "stylesheet",
href: styles,
},
];
};
// ========================================
// The <Links /> component will create the <link ... HTML instruction in the <head> of all pages to load the style sheet
export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<title>MY APPLICATION NAME</title>
<meta name="description" content="MY APPLCIATION DESCRIPTION" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
Menu
To be able to navigate between pages we will create a menu system. To code it only once and use it in every page, we will create the Header component in its own file, and call it from every page of the application. In this example we will use Material UI and React components. After the </AppBar> instruction we will also add a loading indicator which automatically fades in when the page load takes a longer time.
- Install the React and MaterialUI Node.js libraries. In the terminal execute
npm install @mui/material @emotion/react @emotion/styled
- Download or create an animated loading indicator gif image and save it in the /public directory and name it Loading_icon.gif
- Create the _header.tsx file in the /app/routes directory
// /app/routes/_header.tsx file
// Remix imports
import { NavLink, useNavigation } from '@remix-run/react';
// Material UI imports
import AppBar from '@mui/material/AppBar';
// Cascading menu
import * as React from 'react';
import Button from '@mui/material/Button';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
// Export the function to make it available to other modules
export default function Header(environment:any) {
// ----------------------------------------------------
// Loading indicator
const navigation = useNavigation();
const isLoading = Boolean(navigation.state === 'loading');
// ----------------------------------------------------
// Cascading menu
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
// ----------------------------------------------------
return (
<>
<div style={{width: '100%', backgroundColor: '#6ba4ab'}}>
<a
href="./"
rel="noreferrer"
>
<img src="MY_HEADER_IMAGE.png" style={{width: '100%'}} />
</a>
</div>
<AppBar id="navbar" position="static" style={{ display: 'inline-block', backgroundColor: '#6ba4ab'}} >
<div className="navlink">
<NavLink
to="/" end
className={({ isActive, isPending }) =>
isPending ? "pending" : isActive ? "active" : ""
}
>
Home
</NavLink>
</div>
{environment.environment != 'production' ?
<div className="navlink" style={{margin: '0', marginTop: '6px', marginLeft: '12px'}}>
<Button
id="diagrams-button"
aria-controls={open ? 'basic-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
sx={{color: '#f0f0f0', textTransform: 'none', fontFamily: 'Roboto, Helvetica, Arial, sans-serif', fontSize: '1rem', lineHeight: '1', letterSpacing: '0em'}}
>
MY CASCADING MENU ITEMS
</Button>
<Menu
id="basic-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
'aria-labelledby': 'basic-button',
}}
sx={{}}
>
<MenuItem onClick={handleClose}>
<NavLink
to="/physical-supply-chain"
className={({ isActive, isPending }) =>
isPending ? "pending" : isActive ? "active" : ""
}
>
MY CASCADING MENU ITEM NAME
</NavLink>
</MenuItem>
</Menu>
</div>
:
null
}
<div className="navlink">
<NavLink
to="/about"
className={({ isActive, isPending }) =>
isPending ? "pending" : isActive ? "active" : ""
}
>
About this site
</NavLink>
</div>
</AppBar>
{ isLoading ? <div className="fade-in-image" style={{position: 'absolute', zIndex: '100', width: '100vw', height: '100vw'}}><h1 style={{position: 'absolute', top: '20px', left: '100px'}}>Loading data ...</h1><img src="Loading_icon.gif"/></div>
:
null
}
</>
)
}
Add the menu to the home page
- Update the /app/routes/_index.tsx file to call the header and display the menu and remove the sample links:
// /app/routes/_index.tsx file
import type { V2_MetaFunction } from "@remix-run/node";
import Header from "./_header";
export const meta: V2_MetaFunction = () => {
return [
{ title: "New Remix App" },
{ name: "description", content: "Welcome to Remix!" },
];
};
export default function Index() {
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
{/* Display the Header component */}
<Header />
<div className="pagecontent">
Hello
</div>
</div>
);
}
Footer
In the next post we will display the version and the name of the envionment in the footer, so let’s create it with that in mind.
- Create the /app/routes/_footer.tsx file
// /app/routes/_footer.tsx
// Pass the environment variable as
// <Footer environment={environment} />
// Export the function to make it available to other modules
export default function Footer(props:any) {
// React uses camelCase for CSS properties
return (
<>
<div className="footer" style={{marginLeft: '20px'}} >
Version: 2023-09-07_01 {props.environment}
</div>
</>
);
}